ymyy307 commited on
Commit
5a5e271
·
verified ·
1 Parent(s): ac5e7a9

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .github/workflows/logo.gif +3 -0
  3. .github/workflows/publish.yaml +29 -0
  4. .gitignore +178 -0
  5. LICENSE +201 -0
  6. README.md +0 -0
  7. README_zh.md +0 -0
  8. diffsynth/__init__.py +1 -0
  9. diffsynth/configs/__init__.py +2 -0
  10. diffsynth/configs/model_configs.py +0 -0
  11. diffsynth/configs/vram_management_module_maps.py +517 -0
  12. diffsynth/core/__init__.py +8 -0
  13. diffsynth/core/attention/__init__.py +1 -0
  14. diffsynth/core/attention/attention.py +255 -0
  15. diffsynth/core/data/__init__.py +1 -0
  16. diffsynth/core/data/operators.py +341 -0
  17. diffsynth/core/data/unified_dataset.py +140 -0
  18. diffsynth/core/device/__init__.py +2 -0
  19. diffsynth/core/device/npu_compatible_device.py +109 -0
  20. diffsynth/core/gradient/__init__.py +1 -0
  21. diffsynth/core/gradient/gradient_checkpoint.py +65 -0
  22. diffsynth/core/loader/__init__.py +3 -0
  23. diffsynth/core/loader/config.py +177 -0
  24. diffsynth/core/loader/file.py +151 -0
  25. diffsynth/core/loader/model.py +167 -0
  26. diffsynth/core/npu_patch/npu_fused_operator.py +30 -0
  27. diffsynth/core/offload_training/__init__.py +1 -0
  28. diffsynth/core/offload_training/manager.py +177 -0
  29. diffsynth/core/offload_training/memory_buffer.py +136 -0
  30. diffsynth/core/offload_training/offloader.py +71 -0
  31. diffsynth/core/quant/__init__.py +3 -0
  32. diffsynth/core/quant/backends/__init__.py +28 -0
  33. diffsynth/core/quant/backends/bitsandbytes.py +155 -0
  34. diffsynth/core/quant/backends/comfy_kitchen.py +318 -0
  35. diffsynth/core/quant/backends/torchao.py +162 -0
  36. diffsynth/core/quant/base.py +243 -0
  37. diffsynth/core/quant/config.py +570 -0
  38. diffsynth/core/vram/__init__.py +2 -0
  39. diffsynth/core/vram/disk_map.py +93 -0
  40. diffsynth/core/vram/initialization.py +21 -0
  41. diffsynth/core/vram/layers.py +620 -0
  42. diffsynth/diffusion/__init__.py +7 -0
  43. diffsynth/diffusion/base_pipeline.py +542 -0
  44. diffsynth/diffusion/ddim_scheduler.py +109 -0
  45. diffsynth/diffusion/dmd2.py +601 -0
  46. diffsynth/diffusion/flow_match.py +442 -0
  47. diffsynth/diffusion/logger.py +170 -0
  48. diffsynth/diffusion/loss.py +225 -0
  49. diffsynth/diffusion/parsers.py +140 -0
  50. diffsynth/diffusion/runner.py +645 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* 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
 
 
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
+ .github/workflows/logo.gif filter=lfs diff=lfs merge=lfs -text
.github/workflows/logo.gif ADDED

Git LFS Details

  • SHA256: 36a7627b7f0f0a508ec64aba72e5d95d38dfe7958bd8cf42d2a63f6ac2641529
  • Pointer size: 131 Bytes
  • Size of remote file: 149 kB
.github/workflows/publish.yaml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v**'
7
+
8
+ concurrency:
9
+ group: ${{ github.workflow }}-${{ github.ref }}-publish
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ build-n-publish:
14
+ runs-on: ubuntu-20.04
15
+ #if: startsWith(github.event.ref, 'refs/tags')
16
+ steps:
17
+ - uses: actions/checkout@v2
18
+ - name: Set up Python 3.10
19
+ uses: actions/setup-python@v2
20
+ with:
21
+ python-version: '3.10'
22
+ - name: Install wheel
23
+ run: pip install wheel==0.44.0 && pip install -r requirements.txt
24
+ - name: Build DiffSynth
25
+ run: python -m build
26
+ - name: Publish package to PyPI
27
+ run: |
28
+ pip install twine
29
+ twine upload dist/* --skip-existing -u __token__ -p ${{ secrets.PYPI_API_TOKEN }}
.gitignore ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /data
2
+ /models
3
+ /scripts
4
+ /diffusers
5
+ /.vscode
6
+ /.opencode
7
+ *.pkl
8
+ *.safetensors
9
+ *.pth
10
+ *.ckpt
11
+ *.pt
12
+ *.bin
13
+ *.DS_Store
14
+ *.msc
15
+ *.mv
16
+ log*.txt
17
+ AGENTS.md
18
+
19
+ # Byte-compiled / optimized / DLL files
20
+ __pycache__/
21
+ *.py[cod]
22
+ *$py.class
23
+
24
+ # C extensions
25
+ *.so
26
+
27
+ # Distribution / packaging
28
+ .Python
29
+ build/
30
+ develop-eggs/
31
+ dist/
32
+ downloads/
33
+ eggs/
34
+ .eggs/
35
+ lib/
36
+ lib64/
37
+ parts/
38
+ sdist/
39
+ var/
40
+ wheels/
41
+ share/python-wheels/
42
+ *.egg-info/
43
+ .installed.cfg
44
+ *.egg
45
+ MANIFEST
46
+
47
+ # PyInstaller
48
+ # Usually these files are written by a python script from a template
49
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
50
+ *.manifest
51
+ *.spec
52
+
53
+ # Installer logs
54
+ pip-log.txt
55
+ pip-delete-this-directory.txt
56
+
57
+ # Unit test / coverage reports
58
+ htmlcov/
59
+ .tox/
60
+ .nox/
61
+ .coverage
62
+ .coverage.*
63
+ .cache
64
+ nosetests.xml
65
+ coverage.xml
66
+ *.cover
67
+ *.py,cover
68
+ .hypothesis/
69
+ .pytest_cache/
70
+ cover/
71
+
72
+ # Translations
73
+ *.mo
74
+ *.pot
75
+
76
+ # Django stuff:
77
+ *.log
78
+ local_settings.py
79
+ db.sqlite3
80
+ db.sqlite3-journal
81
+
82
+ # Flask stuff:
83
+ instance/
84
+ .webassets-cache
85
+
86
+ # Scrapy stuff:
87
+ .scrapy
88
+
89
+ # Sphinx documentation
90
+ docs/_build/
91
+
92
+ # PyBuilder
93
+ .pybuilder/
94
+ target/
95
+
96
+ # Jupyter Notebook
97
+ .ipynb_checkpoints
98
+
99
+ # IPython
100
+ profile_default/
101
+ ipython_config.py
102
+
103
+ # pyenv
104
+ # For a library or package, you might want to ignore these files since the code is
105
+ # intended to run in multiple environments; otherwise, check them in:
106
+ # .python-version
107
+
108
+ # pipenv
109
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
110
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
111
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
112
+ # install all needed dependencies.
113
+ #Pipfile.lock
114
+
115
+ # poetry
116
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
117
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
118
+ # commonly ignored for libraries.
119
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
120
+ #poetry.lock
121
+
122
+ # pdm
123
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
124
+ #pdm.lock
125
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
126
+ # in version control.
127
+ # https://pdm.fming.dev/#use-with-ide
128
+ .pdm.toml
129
+
130
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
131
+ __pypackages__/
132
+
133
+ # Celery stuff
134
+ celerybeat-schedule
135
+ celerybeat.pid
136
+
137
+ # SageMath parsed files
138
+ *.sage.py
139
+
140
+ # Environments
141
+ .env
142
+ .venv
143
+ env/
144
+ venv/
145
+ ENV/
146
+ env.bak/
147
+ venv.bak/
148
+
149
+ # Spyder project settings
150
+ .spyderproject
151
+ .spyproject
152
+
153
+ # Rope project settings
154
+ .ropeproject
155
+
156
+ # mkdocs documentation
157
+ /site
158
+
159
+ # mypy
160
+ .mypy_cache/
161
+ .dmypy.json
162
+ dmypy.json
163
+
164
+ # Pyre type checker
165
+ .pyre/
166
+
167
+ # pytype static type analyzer
168
+ .pytype/
169
+
170
+ # Cython debug symbols
171
+ cython_debug/
172
+
173
+ # PyCharm
174
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
175
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
176
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
177
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
178
+ #.idea/
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [2023] [Zhongjie Duan]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md ADDED
The diff for this file is too large to render. See raw diff
 
README_zh.md ADDED
The diff for this file is too large to render. See raw diff
 
diffsynth/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .core import *
diffsynth/configs/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .model_configs import MODEL_CONFIGS
2
+ from .vram_management_module_maps import VRAM_MANAGEMENT_MODULE_MAPS, VERSION_CHECKER_MAPS
diffsynth/configs/model_configs.py ADDED
The diff for this file is too large to render. See raw diff
 
diffsynth/configs/vram_management_module_maps.py ADDED
@@ -0,0 +1,517 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flux_general_vram_config = {
2
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
3
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
4
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
5
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
6
+ "torch.nn.GroupNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
7
+ "diffsynth.models.general_modules.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
8
+ "diffsynth.models.flux_lora_encoder.LoRALayerBlock": "diffsynth.core.vram.layers.AutoWrappedModule",
9
+ "diffsynth.models.flux_lora_patcher.LoraMerger": "diffsynth.core.vram.layers.AutoWrappedModule",
10
+ }
11
+
12
+ VRAM_MANAGEMENT_MODULE_MAPS = {
13
+ "diffsynth.models.minimax_h3_dit.MiniMaxH3DiT": {
14
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
15
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
16
+ },
17
+ "diffsynth.models.minimax_h3_dit_comfy.MiniMaxH3DiTComfy": {
18
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
19
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
20
+ },
21
+ "diffsynth.models.minimax_h3_dit_comfy.MiniMaxH3DiTComfyPruned": {
22
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
23
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
24
+ },
25
+ "diffsynth.models.minimax_h3_text_encoder.MiniMaxH3TextEncoder": {
26
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
27
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
28
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
29
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
30
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
31
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextRMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
32
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextRotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
33
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLVisionModel": "diffsynth.core.vram.layers.AutoWrappedModule",
34
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLVisionPatchEmbed": "diffsynth.core.vram.layers.AutoWrappedModule",
35
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLVisionRotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
36
+ },
37
+ "diffsynth.models.minimax_h3_video_vae.MiniMaxH3VideoVAE": {
38
+ "diffsynth.models.minimax_h3_video_vae.WarpedTensor": "diffsynth.core.vram.layers.AutoWrappedModule",
39
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
40
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
41
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
42
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
43
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
44
+ "diffsynth.models.minimax_h3_video_vae.BaseConv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
45
+ "diffsynth.models.minimax_h3_video_vae.TemporalIsolatedSpatialGroupNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
46
+ },
47
+ "diffsynth.models.minimax_h3_audio_vae.MiniMaxH3AudioVAE": {
48
+ "diffsynth.models.minimax_h3_audio_vae.WarpedTensor": "diffsynth.core.vram.layers.AutoWrappedModule",
49
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
50
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
51
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
52
+ "torch.nn.Conv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
53
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
54
+ "diffsynth.models.minimax_h3_audio_vae.Snake": "diffsynth.core.vram.layers.AutoWrappedModule",
55
+ "diffsynth.models.minimax_h3_audio_vae.WeightNormedConv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
56
+ "diffsynth.models.minimax_h3_audio_vae.WeightNormedConvTranspose1d": "diffsynth.core.vram.layers.AutoWrappedModule",
57
+ "diffsynth.models.minimax_h3_audio_vae.UpSample1d": "diffsynth.core.vram.layers.AutoWrappedModule",
58
+ "diffsynth.models.minimax_h3_audio_vae.LowPassFilter1d": "diffsynth.core.vram.layers.AutoWrappedModule",
59
+ },
60
+ "diffsynth.models.qwen_image_dit.QwenImageDiT": {
61
+ "diffsynth.models.qwen_image_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
62
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
63
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
64
+ },
65
+ "diffsynth.models.qwen_video_edit_dit.QwenVideoEditAdapter": {
66
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
67
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
68
+ },
69
+ "diffsynth.models.qwen_image_text_encoder.QwenImageTextEncoder": {
70
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
71
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
72
+ "transformers.models.qwen2_5_vl.modeling_qwen2_5_vl.Qwen2_5_VLRotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
73
+ "transformers.models.qwen2_5_vl.modeling_qwen2_5_vl.Qwen2RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
74
+ "transformers.models.qwen2_5_vl.modeling_qwen2_5_vl.Qwen2_5_VisionPatchEmbed": "diffsynth.core.vram.layers.AutoWrappedModule",
75
+ "transformers.models.qwen2_5_vl.modeling_qwen2_5_vl.Qwen2_5_VisionRotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
76
+ },
77
+ "diffsynth.models.qwen_image_vae.QwenImageVAE": {
78
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
79
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
80
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
81
+ "diffsynth.models.qwen_image_vae.QwenImageRMS_norm": "diffsynth.core.vram.layers.AutoWrappedModule",
82
+ },
83
+ "diffsynth.models.qwen_image_controlnet.BlockWiseControlBlock": {
84
+ "diffsynth.models.qwen_image_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
85
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
86
+ },
87
+ "diffsynth.models.siglip2_image_encoder.Siglip2ImageEncoder": {
88
+ "transformers.models.siglip.modeling_siglip.SiglipVisionEmbeddings": "diffsynth.core.vram.layers.AutoWrappedModule",
89
+ "transformers.models.siglip.modeling_siglip.SiglipMultiheadAttentionPoolingHead": "diffsynth.core.vram.layers.AutoWrappedModule",
90
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
91
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
92
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
93
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
94
+ },
95
+ "diffsynth.models.dinov3_image_encoder.DINOv3ImageEncoder": {
96
+ "transformers.models.dinov3_vit.modeling_dinov3_vit.DINOv3ViTLayerScale": "diffsynth.core.vram.layers.AutoWrappedModule",
97
+ "transformers.models.dinov3_vit.modeling_dinov3_vit.DINOv3ViTRopePositionEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
98
+ "transformers.models.dinov3_vit.modeling_dinov3_vit.DINOv3ViTEmbeddings": "diffsynth.core.vram.layers.AutoWrappedModule",
99
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
100
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
101
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
102
+ },
103
+ "diffsynth.models.qwen_image_image2lora.QwenImageImage2LoRAModel": {
104
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
105
+ },
106
+ "diffsynth.models.wan_video_animate_adapter.WanAnimateAdapter": {
107
+ "diffsynth.models.wan_video_animate_adapter.FaceEncoder": "diffsynth.core.vram.layers.AutoWrappedModule",
108
+ "diffsynth.models.wan_video_animate_adapter.EqualLinear": "diffsynth.core.vram.layers.AutoWrappedModule",
109
+ "diffsynth.models.wan_video_animate_adapter.ConvLayer": "diffsynth.core.vram.layers.AutoWrappedModule",
110
+ "diffsynth.models.wan_video_animate_adapter.FusedLeakyReLU": "diffsynth.core.vram.layers.AutoWrappedModule",
111
+ "diffsynth.models.wan_video_animate_adapter.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
112
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
113
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
114
+ "torch.nn.Conv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
115
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
116
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
117
+ },
118
+ "diffsynth.models.wan_video_dit_s2v.WanS2VModel": {
119
+ "diffsynth.models.wan_video_dit.Head": "diffsynth.core.vram.layers.AutoWrappedModule",
120
+ "diffsynth.models.wan_video_dit_s2v.WanS2VDiTBlock": "diffsynth.core.vram.layers.AutoWrappedModule",
121
+ "diffsynth.models.wan_video_dit_s2v.CausalAudioEncoder": "diffsynth.core.vram.layers.AutoWrappedModule",
122
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
123
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
124
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
125
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
126
+ "diffsynth.models.wan_video_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
127
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
128
+ },
129
+ "diffsynth.models.wan_animate_2_dit.WanAnimate2Transformer": {
130
+ "diffsynth.models.wan_video_dit.MLP": "diffsynth.core.vram.layers.AutoWrappedModule",
131
+ "diffsynth.models.wan_animate_2_dit.AttentionBlock": "diffsynth.core.vram.layers.AutoWrappedNonRecurseModule",
132
+ "diffsynth.models.wan_animate_2_dit.Head": "diffsynth.core.vram.layers.AutoWrappedModule",
133
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
134
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
135
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
136
+ "diffsynth.models.wan_video_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
137
+ },
138
+ "diffsynth.models.wan_video_dit.WanModel": {
139
+ "diffsynth.models.wan_video_dit.MLP": "diffsynth.core.vram.layers.AutoWrappedModule",
140
+ "diffsynth.models.wan_video_dit.DiTBlock": "diffsynth.core.vram.layers.AutoWrappedNonRecurseModule",
141
+ "diffsynth.models.wan_video_dit.Head": "diffsynth.core.vram.layers.AutoWrappedModule",
142
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
143
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
144
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
145
+ "diffsynth.models.wan_video_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
146
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
147
+ },
148
+ "diffsynth.models.wan_video_image_encoder.WanImageEncoder": {
149
+ "diffsynth.models.wan_video_image_encoder.VisionTransformer": "diffsynth.core.vram.layers.AutoWrappedModule",
150
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
151
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
152
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
153
+ },
154
+ "diffsynth.models.wan_video_mot.MotWanModel": {
155
+ "diffsynth.models.wan_video_mot.MotWanAttentionBlock": "diffsynth.core.vram.layers.AutoWrappedModule",
156
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
157
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
158
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
159
+ },
160
+ "diffsynth.models.wan_video_motion_controller.WanMotionControllerModel": {
161
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
162
+ },
163
+ "diffsynth.models.wan_video_text_encoder.WanTextEncoder": {
164
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
165
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
166
+ "diffsynth.models.wan_video_text_encoder.T5RelativeEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
167
+ "diffsynth.models.wan_video_text_encoder.T5LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
168
+ },
169
+ "diffsynth.models.wan_video_vace.VaceWanModel": {
170
+ "diffsynth.models.wan_video_dit.DiTBlock": "diffsynth.core.vram.layers.AutoWrappedModule",
171
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
172
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
173
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
174
+ "diffsynth.models.wan_video_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
175
+ },
176
+ "diffsynth.models.wan_video_vae.WanVideoVAE": {
177
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
178
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
179
+ "diffsynth.models.wan_video_vae.RMS_norm": "diffsynth.core.vram.layers.AutoWrappedModule",
180
+ "diffsynth.models.wan_video_vae.CausalConv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
181
+ "diffsynth.models.wan_video_vae.Upsample": "diffsynth.core.vram.layers.AutoWrappedModule",
182
+ "torch.nn.SiLU": "diffsynth.core.vram.layers.AutoWrappedModule",
183
+ "torch.nn.Dropout": "diffsynth.core.vram.layers.AutoWrappedModule",
184
+ },
185
+ "diffsynth.models.wan_video_vae.WanVideoVAE38": {
186
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
187
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
188
+ "diffsynth.models.wan_video_vae.RMS_norm": "diffsynth.core.vram.layers.AutoWrappedModule",
189
+ "diffsynth.models.wan_video_vae.CausalConv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
190
+ "diffsynth.models.wan_video_vae.Upsample": "diffsynth.core.vram.layers.AutoWrappedModule",
191
+ "torch.nn.SiLU": "diffsynth.core.vram.layers.AutoWrappedModule",
192
+ "torch.nn.Dropout": "diffsynth.core.vram.layers.AutoWrappedModule",
193
+ },
194
+ "diffsynth.models.wav2vec.WanS2VAudioEncoder": {
195
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
196
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
197
+ "torch.nn.Conv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
198
+ },
199
+ "diffsynth.models.longcat_video_dit.LongCatVideoTransformer3DModel": {
200
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
201
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
202
+ "diffsynth.models.longcat_video_dit.RMSNorm_FP32": "diffsynth.core.vram.layers.AutoWrappedModule",
203
+ "diffsynth.models.longcat_video_dit.LayerNorm_FP32": "diffsynth.core.vram.layers.AutoWrappedModule",
204
+ },
205
+ "diffsynth.models.flux_dit.FluxDiT": {
206
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
207
+ "diffsynth.models.flux_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
208
+ },
209
+ "diffsynth.models.flux_text_encoder_clip.FluxTextEncoderClip": flux_general_vram_config,
210
+ "diffsynth.models.flux_vae.FluxVAEEncoder": flux_general_vram_config,
211
+ "diffsynth.models.flux_vae.FluxVAEDecoder": flux_general_vram_config,
212
+ "diffsynth.models.flux_controlnet.FluxControlNet": flux_general_vram_config,
213
+ "diffsynth.models.flux_infiniteyou.InfiniteYouImageProjector": flux_general_vram_config,
214
+ "diffsynth.models.flux_ipadapter.FluxIpAdapter": flux_general_vram_config,
215
+ "diffsynth.models.flux_lora_patcher.FluxLoraPatcher": flux_general_vram_config,
216
+ "diffsynth.models.step1x_connector.Qwen2Connector": flux_general_vram_config,
217
+ "diffsynth.models.flux_lora_encoder.FluxLoRAEncoder": flux_general_vram_config,
218
+ "diffsynth.models.flux_text_encoder_t5.FluxTextEncoderT5": {
219
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
220
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
221
+ "transformers.models.t5.modeling_t5.T5LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
222
+ "transformers.models.t5.modeling_t5.T5DenseActDense": "diffsynth.core.vram.layers.AutoWrappedModule",
223
+ "transformers.models.t5.modeling_t5.T5DenseGatedActDense": "diffsynth.core.vram.layers.AutoWrappedModule",
224
+ },
225
+ "diffsynth.models.flux_ipadapter.SiglipVisionModelSO400M": {
226
+ "transformers.models.siglip.modeling_siglip.SiglipVisionEmbeddings": "diffsynth.core.vram.layers.AutoWrappedModule",
227
+ "transformers.models.siglip.modeling_siglip.SiglipEncoder": "diffsynth.core.vram.layers.AutoWrappedModule",
228
+ "transformers.models.siglip.modeling_siglip.SiglipMultiheadAttentionPoolingHead": "diffsynth.core.vram.layers.AutoWrappedModule",
229
+ "torch.nn.MultiheadAttention": "diffsynth.core.vram.layers.AutoWrappedModule",
230
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
231
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
232
+ },
233
+ "diffsynth.models.flux2_dit.Flux2DiT": {
234
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
235
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
236
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
237
+ },
238
+ "diffsynth.models.flux2_text_encoder.Flux2TextEncoder": {
239
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
240
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
241
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
242
+ "transformers.models.mistral.modeling_mistral.MistralRMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
243
+ },
244
+ "diffsynth.models.flux2_vae.Flux2VAE": {
245
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
246
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
247
+ "torch.nn.GroupNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
248
+ },
249
+ "diffsynth.models.z_image_text_encoder.ZImageTextEncoder": {
250
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
251
+ "transformers.models.qwen3.modeling_qwen3.Qwen3RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
252
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
253
+ },
254
+ "diffsynth.models.z_image_dit.ZImageDiT": {
255
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
256
+ "diffsynth.models.z_image_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
257
+ },
258
+ "diffsynth.models.z_image_controlnet.ZImageControlNet": {
259
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
260
+ "diffsynth.models.z_image_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
261
+ },
262
+ "diffsynth.models.z_image_image2lora.ZImageImage2LoRAModel": {
263
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
264
+ },
265
+ "diffsynth.models.siglip2_image_encoder.Siglip2ImageEncoder428M": {
266
+ "transformers.models.siglip2.modeling_siglip2.Siglip2VisionEmbeddings": "diffsynth.core.vram.layers.AutoWrappedModule",
267
+ "transformers.models.siglip2.modeling_siglip2.Siglip2MultiheadAttentionPoolingHead": "diffsynth.core.vram.layers.AutoWrappedModule",
268
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
269
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
270
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
271
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
272
+ },
273
+ "diffsynth.models.ltx2_dit.LTXModel": {
274
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
275
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
276
+ },
277
+ "diffsynth.models.ltx2_upsampler.LTX2LatentUpsampler": {
278
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
279
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
280
+ "torch.nn.GroupNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
281
+ },
282
+ "diffsynth.models.ltx2_video_vae.LTX2VideoEncoder": {
283
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
284
+ },
285
+ "diffsynth.models.ltx2_video_vae.LTX2VideoDecoder": {
286
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
287
+ },
288
+ "diffsynth.models.ltx2_audio_vae.LTX2AudioDecoder": {
289
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
290
+ },
291
+ "diffsynth.models.ltx2_audio_vae.LTX2Vocoder": {
292
+ "torch.nn.Conv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
293
+ "torch.nn.ConvTranspose1d": "diffsynth.core.vram.layers.AutoWrappedModule",
294
+ },
295
+ "diffsynth.models.ltx2_text_encoder.LTX2TextEncoderPostModules": {
296
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
297
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
298
+ "diffsynth.models.ltx2_text_encoder.Embeddings1DConnector": "diffsynth.core.vram.layers.AutoWrappedModule",
299
+ },
300
+ "diffsynth.models.ltx2_text_encoder.LTX2TextEncoder": {
301
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
302
+ "transformers.models.gemma3.modeling_gemma3.Gemma3MultiModalProjector": "diffsynth.core.vram.layers.AutoWrappedModule",
303
+ "transformers.models.gemma3.modeling_gemma3.Gemma3RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
304
+ "transformers.models.gemma3.modeling_gemma3.Gemma3TextScaledWordEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
305
+ },
306
+ "diffsynth.models.anima_dit.AnimaDiT": {
307
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
308
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
309
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
310
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
311
+ },
312
+ "diffsynth.models.mova_audio_dit.MovaAudioDit": {
313
+ "diffsynth.models.wan_video_dit.DiTBlock": "diffsynth.core.vram.layers.AutoWrappedNonRecurseModule",
314
+ "diffsynth.models.wan_video_dit.Head": "diffsynth.core.vram.layers.AutoWrappedModule",
315
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
316
+ "torch.nn.Conv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
317
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
318
+ "diffsynth.models.wan_video_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
319
+ },
320
+ "diffsynth.models.mova_dual_tower_bridge.DualTowerConditionalBridge": {
321
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
322
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
323
+ "diffsynth.models.wan_video_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
324
+ },
325
+ "diffsynth.models.mova_audio_vae.DacVAE": {
326
+ "diffsynth.models.mova_audio_vae.Snake1d": "diffsynth.core.vram.layers.AutoWrappedModule",
327
+ "torch.nn.Conv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
328
+ "torch.nn.ConvTranspose1d": "diffsynth.core.vram.layers.AutoWrappedModule",
329
+ },
330
+ "diffsynth.models.ernie_image_dit.ErnieImageDiT": {
331
+ "diffsynth.models.ernie_image_dit.ErnieImageRMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
332
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
333
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
334
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
335
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
336
+ },
337
+ "diffsynth.models.ernie_image_text_encoder.ErnieImageTextEncoder": {
338
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
339
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
340
+ "transformers.models.ministral3.modeling_ministral3.Ministral3RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
341
+ },
342
+ "diffsynth.models.joyai_image_dit.Transformer3DModel": {
343
+ "diffsynth.models.joyai_image_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
344
+ "diffsynth.models.joyai_image_dit.ModulateWan": "diffsynth.core.vram.layers.AutoWrappedModule",
345
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
346
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
347
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
348
+ },
349
+ "diffsynth.models.joyai_image_text_encoder.JoyAIImageTextEncoder": {
350
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
351
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
352
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
353
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
354
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLVisionModel": "diffsynth.core.vram.layers.AutoWrappedModule",
355
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextRMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
356
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextRotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
357
+ },
358
+ "diffsynth.models.boogu_image_dit.BooguImageDiT": {
359
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
360
+ "torch.nn.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
361
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
362
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
363
+ },
364
+ "diffsynth.models.ace_step_dit.AceStepDiTModel": {
365
+ "diffsynth.models.ace_step_dit.AceStepDiTLayer": "diffsynth.core.vram.layers.AutoWrappedNonRecurseModule",
366
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
367
+ "torch.nn.Conv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
368
+ "torch.nn.ConvTranspose1d": "diffsynth.core.vram.layers.AutoWrappedModule",
369
+ "transformers.models.qwen3.modeling_qwen3.Qwen3RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
370
+ "transformers.models.qwen3.modeling_qwen3.Qwen3RotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
371
+ },
372
+ "diffsynth.models.ace_step_conditioner.AceStepConditionEncoder": {
373
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
374
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
375
+ "transformers.models.qwen3.modeling_qwen3.Qwen3RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
376
+ "transformers.models.qwen3.modeling_qwen3.Qwen3RotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
377
+ },
378
+ "diffsynth.models.ace_step_text_encoder.AceStepTextEncoder": {
379
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
380
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
381
+ "transformers.models.qwen3.modeling_qwen3.Qwen3RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
382
+ "transformers.models.qwen3.modeling_qwen3.Qwen3RotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
383
+ },
384
+ "diffsynth.models.ace_step_vae.AceStepVAE": {
385
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
386
+ "torch.nn.Conv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
387
+ "torch.nn.ConvTranspose1d": "diffsynth.core.vram.layers.AutoWrappedModule",
388
+ "diffsynth.models.ace_step_vae.Snake1d": "diffsynth.core.vram.layers.AutoWrappedModule",
389
+ },
390
+ "diffsynth.models.ace_step_tokenizer.AceStepTokenizer": {
391
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
392
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
393
+ "diffsynth.models.ace_step_residual_fsq.ResidualFSQ": "diffsynth.core.vram.layers.AutoWrappedModule",
394
+ "transformers.models.qwen3.modeling_qwen3.Qwen3RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
395
+ "transformers.models.qwen3.modeling_qwen3.Qwen3MLP": "diffsynth.core.vram.layers.AutoWrappedModule",
396
+ "transformers.models.qwen3.modeling_qwen3.Qwen3RotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
397
+ },
398
+ "diffsynth.models.stable_diffusion_unet.UNet2DConditionModel": {
399
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
400
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
401
+ "torch.nn.GroupNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
402
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
403
+ "torch.nn.SiLU": "diffsynth.core.vram.layers.AutoWrappedModule",
404
+ "torch.nn.Dropout": "diffsynth.core.vram.layers.AutoWrappedModule",
405
+ },
406
+ "diffsynth.models.stable_diffusion_vae.StableDiffusionVAE": {
407
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
408
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
409
+ "torch.nn.GroupNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
410
+ "torch.nn.SiLU": "diffsynth.core.vram.layers.AutoWrappedModule",
411
+ "torch.nn.Dropout": "diffsynth.core.vram.layers.AutoWrappedModule",
412
+ "diffsynth.models.stable_diffusion_vae.Upsample2D": "diffsynth.core.vram.layers.AutoWrappedModule",
413
+ "diffsynth.models.stable_diffusion_vae.Downsample2D": "diffsynth.core.vram.layers.AutoWrappedModule",
414
+ },
415
+ "diffsynth.models.stable_diffusion_text_encoder.SDTextEncoder": {
416
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
417
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
418
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
419
+ "diffsynth.models.stable_diffusion_text_encoder.CLIPEncoderLayer": "diffsynth.core.vram.layers.AutoWrappedModule",
420
+ "diffsynth.models.stable_diffusion_text_encoder.CLIPAttention": "diffsynth.core.vram.layers.AutoWrappedModule",
421
+ },
422
+ "diffsynth.models.stable_diffusion_xl_unet.SDXLUNet2DConditionModel": {
423
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
424
+ "torch.nn.Conv2d": "diffsynth.core.vram.layers.AutoWrappedModule",
425
+ "torch.nn.GroupNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
426
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
427
+ "torch.nn.SiLU": "diffsynth.core.vram.layers.AutoWrappedModule",
428
+ "torch.nn.Dropout": "diffsynth.core.vram.layers.AutoWrappedModule",
429
+ },
430
+ "diffsynth.models.stable_diffusion_xl_text_encoder.SDXLTextEncoder2": {
431
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
432
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
433
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
434
+ "diffsynth.models.stable_diffusion_text_encoder.CLIPEncoderLayer": "diffsynth.core.vram.layers.AutoWrappedModule",
435
+ "diffsynth.models.stable_diffusion_text_encoder.CLIPAttention": "diffsynth.core.vram.layers.AutoWrappedModule",
436
+ "diffsynth.models.stable_diffusion_xl_text_encoder.CLIPTextModelWithProjection": "diffsynth.core.vram.layers.AutoWrappedModule",
437
+ },
438
+ "diffsynth.models.hidream_o1_image_dit.HiDreamO1ImageModel": {
439
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
440
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
441
+ "torch.nn.Conv3d": "diffsynth.core.vram.layers.AutoWrappedModule",
442
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
443
+ "diffsynth.models.hidream_o1_image_dit.Qwen3VLTextRMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
444
+ "diffsynth.models.hidream_o1_image_dit.Qwen3VLVisionModel": "diffsynth.core.vram.layers.AutoWrappedModule",
445
+ },
446
+ "diffsynth.models.ideogram4_dit.Ideogram4DiT": {
447
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
448
+ "diffsynth.models.ideogram4_dit.Ideogram4RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
449
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
450
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
451
+ },
452
+ "diffsynth.models.ideogram4_text_encoder.Ideogram4TextEncoder": {
453
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
454
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
455
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
456
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextRotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
457
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextRMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
458
+ },
459
+ "diffsynth.models.krea2_dit.SingleStreamDiT": {
460
+ "diffsynth.models.krea2_dit.RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
461
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
462
+ "diffsynth.models.krea2_dit.DoubleSharedModulation": "diffsynth.core.vram.layers.AutoWrappedModule",
463
+ "diffsynth.models.krea2_dit.SimpleModulation": "diffsynth.core.vram.layers.AutoWrappedModule",
464
+ },
465
+ "diffsynth.models.krea2_text_encoder.Krea2TextEncoder": {
466
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
467
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
468
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextRotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
469
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLTextRMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
470
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
471
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLVisionPatchEmbed": "diffsynth.core.vram.layers.AutoWrappedModule",
472
+ "transformers.models.qwen3_vl.modeling_qwen3_vl.Qwen3VLVisionRotaryEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
473
+ },
474
+ "diffsynth.models.lingbot_video_dit.LingBotVideoDiT": {
475
+ "diffsynth.models.lingbot_video_dit.LingBotVideoBlock": "diffsynth.core.vram.layers.AutoWrappedNonRecurseModule",
476
+ "diffsynth.models.lingbot_video_dit.LingBotVideoRouter": "diffsynth.core.vram.layers.AutoWrappedNonRecurseModule",
477
+ "diffsynth.models.lingbot_video_dit.LingBotVideoGroupedExperts": "diffsynth.core.vram.layers.AutoWrappedNonRecurseModule",
478
+ "diffsynth.models.lingbot_video_dit.LingBotVideoRMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
479
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
480
+ },
481
+ "diffsynth.models.minimax_music3_dit.MiniMaxMusic3DiT": {
482
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
483
+ "torch.nn.Conv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
484
+ "torch.nn.LayerNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
485
+ "diffsynth.models.minimax_music3_dit.MiniMaxMusic3FourierEmbedding": "diffsynth.core.vram.layers.AutoWrappedModule",
486
+ },
487
+ "diffsynth.models.minimax_music3_rvq_depth_decoder.MiniMaxMusic3RVQDepthDecoder": {
488
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
489
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
490
+ "diffsynth.models.minimax_music3_rvq_depth_decoder.MiniMaxMusic3RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
491
+ },
492
+ "diffsynth.models.minimax_music3_vocoder.MiniMaxMusic3Vocoder": {
493
+ "torch.nn.Conv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
494
+ "diffsynth.models.minimax_music3_vocoder.MiniMaxMusic3WeightNormConv1d": "diffsynth.core.vram.layers.AutoWrappedModule",
495
+ "diffsynth.models.minimax_music3_vocoder.MiniMaxMusic3WeightNormConvTranspose1d": "diffsynth.core.vram.layers.AutoWrappedModule",
496
+ "diffsynth.models.minimax_music3_vocoder.MiniMaxMusic3Snake1d": "diffsynth.core.vram.layers.AutoWrappedModule",
497
+ },
498
+ "diffsynth.models.minimax_music3_text_encoder.MiniMaxMusic3TextEncoder": {
499
+ "torch.nn.Linear": "diffsynth.core.vram.layers.AutoWrappedLinear",
500
+ "torch.nn.Embedding": "diffsynth.core.vram.layers.AutoWrappedModule",
501
+ "transformers.models.qwen3.modeling_qwen3.Qwen3RMSNorm": "diffsynth.core.vram.layers.AutoWrappedModule",
502
+ },
503
+ }
504
+
505
+ def QwenImageTextEncoder_Module_Map_Updater():
506
+ current = VRAM_MANAGEMENT_MODULE_MAPS["diffsynth.models.qwen_image_text_encoder.QwenImageTextEncoder"]
507
+ from packaging import version
508
+ import transformers
509
+ if version.parse(transformers.__version__) >= version.parse("5.2.0"):
510
+ # The Qwen2RMSNorm in transformers 5.2.0+ has been renamed to Qwen2_5_VLRMSNorm, so we need to update the module map accordingly
511
+ current.pop("transformers.models.qwen2_5_vl.modeling_qwen2_5_vl.Qwen2RMSNorm", None)
512
+ current["transformers.models.qwen2_5_vl.modeling_qwen2_5_vl.Qwen2_5_VLRMSNorm"] = "diffsynth.core.vram.layers.AutoWrappedModule"
513
+ return current
514
+
515
+ VERSION_CHECKER_MAPS = {
516
+ "diffsynth.models.qwen_image_text_encoder.QwenImageTextEncoder": QwenImageTextEncoder_Module_Map_Updater,
517
+ }
diffsynth/core/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from .attention import *
2
+ from .data import *
3
+ from .gradient import *
4
+ from .quant import *
5
+ from .loader import *
6
+ from .vram import *
7
+ from .device import *
8
+ from .offload_training import *
diffsynth/core/attention/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .attention import attention_forward
diffsynth/core/attention/attention.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch, os, inspect
2
+ from einops import rearrange, repeat
3
+
4
+ if os.environ.get("DIFFSYNTH_FLASH_ATTN_KERNEL_REPO_ID") is not None:
5
+ try:
6
+ from kernels import get_kernel
7
+ kernel_module = get_kernel(os.environ.get("DIFFSYNTH_FLASH_ATTN_KERNEL_REPO_ID"), version=int(os.environ.get("DIFFSYNTH_FLASH_ATTN_KERNEL_VERSION")))
8
+ customized_flash_attn_func = kernel_module.flash_attn_func
9
+ CUSTOMIZED_FA_KERNEL_AVAILABLE = True
10
+ except Exception:
11
+ CUSTOMIZED_FA_KERNEL_AVAILABLE = False
12
+ else:
13
+ CUSTOMIZED_FA_KERNEL_AVAILABLE = False
14
+
15
+ try:
16
+ from flash_attn.cute import flash_attn_func as flash_attn_func_cute
17
+ FLASH_ATTN_4_AVAILABLE = True
18
+ except Exception:
19
+ FLASH_ATTN_4_AVAILABLE = False
20
+
21
+ try:
22
+ import flash_attn_interface
23
+ FLASH_ATTN_3_AVAILABLE = True
24
+ except Exception:
25
+ FLASH_ATTN_3_AVAILABLE = False
26
+
27
+ try:
28
+ import flash_attn
29
+ FLASH_ATTN_2_AVAILABLE = True
30
+ except Exception:
31
+ FLASH_ATTN_2_AVAILABLE = False
32
+
33
+ try:
34
+ from sageattention import sageattn
35
+ SAGE_ATTN_AVAILABLE = True
36
+ except Exception:
37
+ SAGE_ATTN_AVAILABLE = False
38
+
39
+ try:
40
+ import xformers.ops as xops
41
+ XFORMERS_AVAILABLE = True
42
+ except Exception:
43
+ XFORMERS_AVAILABLE = False
44
+
45
+ try:
46
+ from torch.nn.attention.flex_attention import flex_attention as flex_attention_func
47
+ flex_attention_func = torch.compile(flex_attention_func, dynamic=False, mode="max-autotune-no-cudagraphs", fullgraph=True, backend="inductor")
48
+ FLEX_ATTN_AVAILABLE = True
49
+ except Exception:
50
+ FLEX_ATTN_AVAILABLE = False
51
+
52
+ try:
53
+ if "enable_gqa" in inspect.signature(torch.nn.functional.scaled_dot_product_attention).parameters:
54
+ TORCH_SUPPORT_GQA = True
55
+ else:
56
+ TORCH_SUPPORT_GQA = False
57
+ except Exception:
58
+ TORCH_SUPPORT_GQA = False
59
+
60
+
61
+ def initialize_attention_priority():
62
+ if os.environ.get('DIFFSYNTH_ATTENTION_IMPLEMENTATION') is not None:
63
+ return os.environ.get('DIFFSYNTH_ATTENTION_IMPLEMENTATION').lower()
64
+ elif CUSTOMIZED_FA_KERNEL_AVAILABLE:
65
+ return "customized_fa_kernel"
66
+ elif FLASH_ATTN_4_AVAILABLE:
67
+ return "flash_attention_4"
68
+ elif FLASH_ATTN_3_AVAILABLE:
69
+ return "flash_attention_3"
70
+ elif FLASH_ATTN_2_AVAILABLE:
71
+ return "flash_attention_2"
72
+ elif SAGE_ATTN_AVAILABLE:
73
+ return "sage_attention"
74
+ elif XFORMERS_AVAILABLE:
75
+ return "xformers"
76
+ else:
77
+ return "torch"
78
+
79
+
80
+ ATTENTION_IMPLEMENTATION = initialize_attention_priority()
81
+
82
+
83
+ def rearrange_qkv(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", required_in_pattern="b n s d", dims=None):
84
+ dims = {} if dims is None else dims
85
+ if q_pattern != required_in_pattern:
86
+ q = rearrange(q, f"{q_pattern} -> {required_in_pattern}", **dims)
87
+ if k_pattern != required_in_pattern:
88
+ k = rearrange(k, f"{k_pattern} -> {required_in_pattern}", **dims)
89
+ if v_pattern != required_in_pattern:
90
+ v = rearrange(v, f"{v_pattern} -> {required_in_pattern}", **dims)
91
+ return q, k, v
92
+
93
+
94
+ def rearrange_out(out: torch.Tensor, out_pattern="b n s d", required_out_pattern="b n s d", dims=None):
95
+ dims = {} if dims is None else dims
96
+ if out_pattern != required_out_pattern:
97
+ out = rearrange(out, f"{required_out_pattern} -> {out_pattern}", **dims)
98
+ return out
99
+
100
+
101
+ def torch_sdpa(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", out_pattern="b n s d", dims=None, attn_mask=None, scale=None, is_causal=False):
102
+ required_in_pattern, required_out_pattern= "b n s d", "b n s d"
103
+ q, k, v = rearrange_qkv(q, k, v, q_pattern, k_pattern, v_pattern, required_in_pattern, dims)
104
+ if q.shape[1] != k.shape[1] or q.shape[1] != v.shape[1]:
105
+ # Grouped Query Attention
106
+ if TORCH_SUPPORT_GQA:
107
+ out = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask, scale=scale, is_causal=is_causal, enable_gqa=True)
108
+ else:
109
+ # In low-version torch, `enable_gqa` is not supported.
110
+ k = repeat(k, "b n s d -> b (n m) s d", m=q.shape[1]//k.shape[1])
111
+ v = repeat(v, "b n s d -> b (n m) s d", m=q.shape[1]//v.shape[1])
112
+ out = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask, scale=scale, is_causal=is_causal)
113
+ else:
114
+ out = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask, scale=scale, is_causal=is_causal)
115
+ out = rearrange_out(out, out_pattern, required_out_pattern, dims)
116
+ return out
117
+
118
+
119
+ def torch_sdpa_sliding_window(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, sliding_window: int, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", out_pattern="b n s d", dims=None, scale=None):
120
+ required_in_pattern, required_out_pattern = "b n s d", "b n s d"
121
+ q, k, v = rearrange_qkv(q, k, v, q_pattern, k_pattern, v_pattern, required_in_pattern, dims)
122
+
123
+ B, N, S, D = q.shape
124
+ W = sliding_window
125
+ chunk_size = W
126
+ num_chunks = (S + chunk_size - 1) // chunk_size
127
+
128
+ output = torch.empty_like(q)
129
+ dtype = q.dtype
130
+ device = q.device
131
+ min_val = torch.finfo(dtype).min
132
+
133
+ for i in range(num_chunks):
134
+ q_start = i * chunk_size
135
+ q_end = min(q_start + chunk_size, S)
136
+ actual_chunk_size = q_end - q_start
137
+
138
+ kv_start = max(0, q_start - W)
139
+ kv_end = min(S, q_end + W)
140
+
141
+ q_chunk = q[:, :, q_start:q_end, :]
142
+ k_chunk = k[:, :, kv_start:kv_end, :]
143
+ v_chunk = v[:, :, kv_start:kv_end, :]
144
+
145
+ q_indices = torch.arange(q_start, q_end, device=device)
146
+ k_indices = torch.arange(kv_start, kv_end, device=device)
147
+ diff = q_indices.unsqueeze(1) - k_indices.unsqueeze(0)
148
+ valid = diff.abs() <= W
149
+
150
+ local_mask = torch.zeros(actual_chunk_size, kv_end - kv_start, dtype=dtype, device=device)
151
+ local_mask.masked_fill_(~valid, min_val)
152
+ local_mask = local_mask.unsqueeze(0).unsqueeze(0)
153
+
154
+ out_chunk = torch_sdpa(
155
+ q_chunk, k_chunk, v_chunk, attn_mask=local_mask, scale=scale
156
+ )
157
+ output[:, :, q_start:q_end, :] = out_chunk
158
+
159
+ output = rearrange_out(output, out_pattern, required_out_pattern, dims)
160
+ return output
161
+
162
+
163
+ def flash_attention_4(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", out_pattern="b n s d", dims=None, scale=None, is_causal=False, window_size=None):
164
+ required_in_pattern, required_out_pattern= "b s n d", "b s n d"
165
+ q, k, v = rearrange_qkv(q, k, v, q_pattern, k_pattern, v_pattern, required_in_pattern, dims)
166
+ window_size = (window_size, window_size) if window_size is not None else (-1, -1)
167
+ out = flash_attn_func_cute(q, k, v, softmax_scale=scale, causal=is_causal, window_size=window_size)
168
+ if isinstance(out, tuple):
169
+ out = out[0]
170
+ out = rearrange_out(out, out_pattern, required_out_pattern, dims)
171
+ return out
172
+
173
+
174
+ def flash_attention_3(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", out_pattern="b n s d", dims=None, scale=None, is_causal=False, window_size=None):
175
+ required_in_pattern, required_out_pattern= "b s n d", "b s n d"
176
+ q, k, v = rearrange_qkv(q, k, v, q_pattern, k_pattern, v_pattern, required_in_pattern, dims)
177
+ window_size = (window_size, window_size) if window_size is not None else (-1, -1)
178
+ out = flash_attn_interface.flash_attn_func(q, k, v, softmax_scale=scale, causal=is_causal, window_size=window_size)
179
+ if isinstance(out, tuple):
180
+ out = out[0]
181
+ out = rearrange_out(out, out_pattern, required_out_pattern, dims)
182
+ return out
183
+
184
+
185
+ def flash_attention_2(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", out_pattern="b n s d", dims=None, scale=None, is_causal=False, window_size=None):
186
+ required_in_pattern, required_out_pattern= "b s n d", "b s n d"
187
+ q, k, v = rearrange_qkv(q, k, v, q_pattern, k_pattern, v_pattern, required_in_pattern, dims)
188
+ window_size = (window_size, window_size) if window_size is not None else (-1, -1)
189
+ out = flash_attn.flash_attn_func(q, k, v, softmax_scale=scale, causal=is_causal, window_size=window_size)
190
+ out = rearrange_out(out, out_pattern, required_out_pattern, dims)
191
+ return out
192
+
193
+
194
+ def flash_attention_customized_kernel(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", out_pattern="b n s d", dims=None, scale=None, is_causal=False, window_size=None):
195
+ required_in_pattern, required_out_pattern = "b s n d", "b s n d"
196
+ q, k, v = rearrange_qkv(q, k, v, q_pattern, k_pattern, v_pattern, required_in_pattern, dims)
197
+ window_size = (window_size, window_size) if window_size is not None else (-1, -1)
198
+ out = customized_flash_attn_func(q, k, v, softmax_scale=scale, causal=is_causal, window_size=window_size)
199
+ if isinstance(out, tuple):
200
+ out = out[0]
201
+ out = rearrange_out(out, out_pattern, required_out_pattern, dims)
202
+ return out
203
+
204
+
205
+ def sage_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", out_pattern="b n s d", dims=None, scale=None):
206
+ required_in_pattern, required_out_pattern= "b n s d", "b n s d"
207
+ q, k, v = rearrange_qkv(q, k, v, q_pattern, k_pattern, v_pattern, required_in_pattern, dims)
208
+ out = sageattn(q, k, v, sm_scale=scale)
209
+ out = rearrange_out(out, out_pattern, required_out_pattern, dims)
210
+ return out
211
+
212
+
213
+ def xformers_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", out_pattern="b n s d", dims=None, scale=None):
214
+ required_in_pattern, required_out_pattern= "b s n d", "b s n d"
215
+ q, k, v = rearrange_qkv(q, k, v, q_pattern, k_pattern, v_pattern, required_in_pattern, dims)
216
+ out = xops.memory_efficient_attention(q, k, v, scale=scale)
217
+ out = rearrange_out(out, out_pattern, required_out_pattern, dims)
218
+ return out
219
+
220
+
221
+ def flex_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", out_pattern="b n s d", dims=None, attn_mask=None, scale=None, score_mod=None):
222
+ assert FLEX_ATTN_AVAILABLE, "Flex Attention is not available. Please upgrade torch to 2.5.0 or later."
223
+ required_in_pattern, required_out_pattern = "b n s d", "b n s d"
224
+ q, k, v = rearrange_qkv(q, k, v, q_pattern, k_pattern, v_pattern, required_in_pattern, dims)
225
+ out = flex_attention_func(query=q, key=k, value=v, block_mask=attn_mask, scale=scale, score_mod=score_mod)
226
+ out = rearrange_out(out, out_pattern, required_out_pattern, dims)
227
+ return out
228
+
229
+
230
+ def attention_forward(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, q_pattern="b n s d", k_pattern="b n s d", v_pattern="b n s d", out_pattern="b n s d", dims=None, attn_mask=None, scale=None, is_causal=False, compatibility_mode=False, window_size=None, use_flex=False, score_mod=None):
231
+ if compatibility_mode or (attn_mask is not None) or ATTENTION_IMPLEMENTATION == "torch":
232
+ if use_flex or score_mod is not None:
233
+ return flex_attention(q, k, v, q_pattern, k_pattern, v_pattern, out_pattern, dims, attn_mask=attn_mask, scale=scale, score_mod=score_mod)
234
+ elif window_size is None:
235
+ return torch_sdpa(q, k, v, q_pattern, k_pattern, v_pattern, out_pattern, dims, attn_mask=attn_mask, scale=scale, is_causal=is_causal)
236
+ else:
237
+ # Sliding Window Attention is not compatible with `is_causal` and `attn_mask`.
238
+ assert is_causal == False and attn_mask is None
239
+ return torch_sdpa_sliding_window(q, k, v, window_size, q_pattern, k_pattern, v_pattern, out_pattern, dims, scale=scale)
240
+ elif ATTENTION_IMPLEMENTATION == "customized_fa_kernel":
241
+ return flash_attention_customized_kernel(q, k, v, q_pattern, k_pattern, v_pattern, out_pattern, dims, scale=scale, is_causal=is_causal, window_size=window_size)
242
+ elif ATTENTION_IMPLEMENTATION == "flash_attention_4":
243
+ return flash_attention_4(q, k, v, q_pattern, k_pattern, v_pattern, out_pattern, dims, scale=scale, is_causal=is_causal, window_size=window_size)
244
+ elif ATTENTION_IMPLEMENTATION == "flash_attention_3":
245
+ return flash_attention_3(q, k, v, q_pattern, k_pattern, v_pattern, out_pattern, dims, scale=scale, is_causal=is_causal, window_size=window_size)
246
+ elif ATTENTION_IMPLEMENTATION == "flash_attention_2":
247
+ return flash_attention_2(q, k, v, q_pattern, k_pattern, v_pattern, out_pattern, dims, scale=scale, is_causal=is_causal, window_size=window_size)
248
+ elif ATTENTION_IMPLEMENTATION == "sage_attention":
249
+ if window_size is not None or is_causal: return attention_forward(q, k, v, q_pattern, k_pattern, v_pattern, out_pattern, dims, attn_mask, scale, is_causal, compatibility_mode=True, window_size=window_size)
250
+ return sage_attention(q, k, v, q_pattern, k_pattern, v_pattern, out_pattern, dims, scale=scale)
251
+ elif ATTENTION_IMPLEMENTATION == "xformers":
252
+ if window_size is not None or is_causal: return attention_forward(q, k, v, q_pattern, k_pattern, v_pattern, out_pattern, dims, attn_mask, scale, is_causal, compatibility_mode=True, window_size=window_size)
253
+ return xformers_attention(q, k, v, q_pattern, k_pattern, v_pattern, out_pattern, dims, scale=scale)
254
+ else:
255
+ raise NotImplementedError("No available attention implementation.")
diffsynth/core/data/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .unified_dataset import UnifiedDataset
diffsynth/core/data/operators.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math, warnings, time
2
+ import torch, torchvision, imageio, os
3
+ import imageio.v3 as iio
4
+ import urllib.request
5
+ from io import BytesIO
6
+ from PIL import Image
7
+ from einops import repeat
8
+
9
+
10
+ def _is_url(path):
11
+ return isinstance(path, str) and path.startswith(("http://", "https://"))
12
+
13
+
14
+ def _load_image_from_url(url, timeout=30, max_retries=3):
15
+ request = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (compatible; DiffSynth-Studio)"})
16
+ last_error = None
17
+ for attempt in range(max_retries):
18
+ try:
19
+ with urllib.request.urlopen(request, timeout=timeout) as response:
20
+ image = Image.open(BytesIO(response.read()))
21
+ image.load()
22
+ return image
23
+ except Exception as e:
24
+ last_error = e
25
+ if attempt < max_retries - 1:
26
+ time.sleep(2 ** attempt)
27
+ raise RuntimeError(f"Failed to load image from URL '{url}' after {max_retries} attempts: {last_error}") from last_error
28
+
29
+
30
+ class DataProcessingPipeline:
31
+ def __init__(self, operators=None):
32
+ self.operators: list[DataProcessingOperator] = [] if operators is None else operators
33
+
34
+ def __call__(self, data):
35
+ for operator in self.operators:
36
+ data = operator(data)
37
+ return data
38
+
39
+ def __rshift__(self, pipe):
40
+ if isinstance(pipe, DataProcessingOperator):
41
+ pipe = DataProcessingPipeline([pipe])
42
+ return DataProcessingPipeline(self.operators + pipe.operators)
43
+
44
+
45
+ class DataProcessingOperator:
46
+ def __call__(self, data):
47
+ raise NotImplementedError("DataProcessingOperator cannot be called directly.")
48
+
49
+ def __rshift__(self, pipe):
50
+ if isinstance(pipe, DataProcessingOperator):
51
+ pipe = DataProcessingPipeline([pipe])
52
+ return DataProcessingPipeline([self]).__rshift__(pipe)
53
+
54
+
55
+ class DataProcessingOperatorRaw(DataProcessingOperator):
56
+ def __call__(self, data):
57
+ return data
58
+
59
+
60
+ class ToInt(DataProcessingOperator):
61
+ def __call__(self, data):
62
+ return int(data)
63
+
64
+
65
+ class ToFloat(DataProcessingOperator):
66
+ def __call__(self, data):
67
+ return float(data)
68
+
69
+
70
+ class ToStr(DataProcessingOperator):
71
+ def __init__(self, none_value=""):
72
+ self.none_value = none_value
73
+
74
+ def __call__(self, data):
75
+ if data is None: data = self.none_value
76
+ return str(data)
77
+
78
+
79
+ class LoadImage(DataProcessingOperator):
80
+ def __init__(self, convert_RGB=True, convert_RGBA=False, url_timeout=30, url_max_retries=3):
81
+ self.convert_RGB = convert_RGB
82
+ self.convert_RGBA = convert_RGBA
83
+ self.url_timeout = url_timeout
84
+ self.url_max_retries = url_max_retries
85
+
86
+ def __call__(self, data: str):
87
+ if _is_url(data):
88
+ image = _load_image_from_url(data, timeout=self.url_timeout, max_retries=self.url_max_retries)
89
+ else:
90
+ image = Image.open(data)
91
+ if self.convert_RGB: image = image.convert("RGB")
92
+ if self.convert_RGBA: image = image.convert("RGBA")
93
+ return image
94
+
95
+
96
+ class ImageCropAndResize(DataProcessingOperator):
97
+ def __init__(self, height=None, width=None, max_pixels=None, height_division_factor=1, width_division_factor=1):
98
+ self.height = height
99
+ self.width = width
100
+ self.max_pixels = max_pixels
101
+ self.height_division_factor = height_division_factor
102
+ self.width_division_factor = width_division_factor
103
+
104
+ def crop_and_resize(self, image, target_height, target_width):
105
+ width, height = image.size
106
+ scale = max(target_width / width, target_height / height)
107
+ image = torchvision.transforms.functional.resize(
108
+ image,
109
+ (round(height*scale), round(width*scale)),
110
+ interpolation=torchvision.transforms.InterpolationMode.BILINEAR
111
+ )
112
+ image = torchvision.transforms.functional.center_crop(image, (target_height, target_width))
113
+ return image
114
+
115
+ def get_height_width(self, image):
116
+ if self.height is None or self.width is None:
117
+ width, height = image.size
118
+ if width * height > self.max_pixels:
119
+ scale = (width * height / self.max_pixels) ** 0.5
120
+ height, width = int(height / scale), int(width / scale)
121
+ height = height // self.height_division_factor * self.height_division_factor
122
+ width = width // self.width_division_factor * self.width_division_factor
123
+ else:
124
+ height, width = self.height, self.width
125
+ return height, width
126
+
127
+ def __call__(self, data: Image.Image):
128
+ image = self.crop_and_resize(data, *self.get_height_width(data))
129
+ return image
130
+
131
+
132
+ class ToList(DataProcessingOperator):
133
+ def __call__(self, data):
134
+ return [data]
135
+
136
+
137
+ class FrameSamplerByRateMixin:
138
+ def __init__(self, num_frames=81, time_division_factor=4, time_division_remainder=1, frame_rate=24, fix_frame_rate=False):
139
+ self.num_frames = num_frames
140
+ self.time_division_factor = time_division_factor
141
+ self.time_division_remainder = time_division_remainder
142
+ self.frame_rate = frame_rate
143
+ self.fix_frame_rate = fix_frame_rate
144
+
145
+ def get_reader(self, data: str):
146
+ return imageio.get_reader(data)
147
+
148
+ def get_available_num_frames(self, reader):
149
+ if not self.fix_frame_rate:
150
+ return reader.count_frames()
151
+ meta_data = reader.get_meta_data()
152
+ total_original_frames = int(reader.count_frames())
153
+ duration = meta_data["duration"] if "duration" in meta_data else total_original_frames / meta_data['fps']
154
+ total_available_frames = math.floor(duration * self.frame_rate)
155
+ return int(total_available_frames)
156
+
157
+ def get_num_frames(self, reader):
158
+ num_frames = self.num_frames
159
+ total_frames = self.get_available_num_frames(reader)
160
+ if int(total_frames) < num_frames:
161
+ num_frames = total_frames
162
+ while num_frames > 1 and num_frames % self.time_division_factor != self.time_division_remainder:
163
+ num_frames -= 1
164
+ return num_frames
165
+
166
+ def map_single_frame_id(self, new_sequence_id: int, raw_frame_rate: float, total_raw_frames: int) -> int:
167
+ if not self.fix_frame_rate:
168
+ return new_sequence_id
169
+ target_time_in_seconds = new_sequence_id / self.frame_rate
170
+ raw_frame_index_float = target_time_in_seconds * raw_frame_rate
171
+ frame_id = int(round(raw_frame_index_float))
172
+ frame_id = min(frame_id, total_raw_frames - 1)
173
+ return frame_id
174
+
175
+
176
+ class LoadVideo(DataProcessingOperator, FrameSamplerByRateMixin):
177
+ def __init__(self, num_frames=81, time_division_factor=4, time_division_remainder=1, frame_processor=lambda x: x, frame_rate=24, fix_frame_rate=False):
178
+ FrameSamplerByRateMixin.__init__(self, num_frames, time_division_factor, time_division_remainder, frame_rate, fix_frame_rate)
179
+ # frame_processor is build in the video loader for high efficiency.
180
+ self.frame_processor = frame_processor
181
+
182
+ def __call__(self, data: str):
183
+ reader = self.get_reader(data)
184
+ raw_frame_rate = reader.get_meta_data()['fps']
185
+ num_frames = self.get_num_frames(reader)
186
+ total_raw_frames = reader.count_frames()
187
+ frames = []
188
+ for frame_id in range(num_frames):
189
+ frame_id = self.map_single_frame_id(frame_id, raw_frame_rate, total_raw_frames)
190
+ frame = reader.get_data(frame_id)
191
+ frame = Image.fromarray(frame)
192
+ frame = self.frame_processor(frame)
193
+ frames.append(frame)
194
+ reader.close()
195
+ return frames
196
+
197
+
198
+ class SequencialProcess(DataProcessingOperator):
199
+ def __init__(self, operator=lambda x: x):
200
+ self.operator = operator
201
+
202
+ def __call__(self, data):
203
+ return [self.operator(i) for i in data]
204
+
205
+
206
+ class LoadGIF(DataProcessingOperator):
207
+ def __init__(self, num_frames=81, time_division_factor=4, time_division_remainder=1, frame_processor=lambda x: x):
208
+ self.num_frames = num_frames
209
+ self.time_division_factor = time_division_factor
210
+ self.time_division_remainder = time_division_remainder
211
+ # frame_processor is build in the video loader for high efficiency.
212
+ self.frame_processor = frame_processor
213
+
214
+ def get_num_frames(self, path):
215
+ num_frames = self.num_frames
216
+ images = iio.imread(path, mode="RGB")
217
+ if len(images) < num_frames:
218
+ num_frames = len(images)
219
+ while num_frames > 1 and num_frames % self.time_division_factor != self.time_division_remainder:
220
+ num_frames -= 1
221
+ return num_frames
222
+
223
+ def __call__(self, data: str):
224
+ num_frames = self.get_num_frames(data)
225
+ frames = []
226
+ images = iio.imread(data, mode="RGB")
227
+ for img in images:
228
+ frame = Image.fromarray(img)
229
+ frame = self.frame_processor(frame)
230
+ frames.append(frame)
231
+ if len(frames) >= num_frames:
232
+ break
233
+ return frames
234
+
235
+
236
+ class RouteByExtensionName(DataProcessingOperator):
237
+ def __init__(self, operator_map):
238
+ self.operator_map = operator_map
239
+
240
+ def __call__(self, data: str):
241
+ file_ext_name = data.split(".")[-1].lower()
242
+ for ext_names, operator in self.operator_map:
243
+ if ext_names is None or file_ext_name in ext_names:
244
+ return operator(data)
245
+ raise ValueError(f"Unsupported file: {data}")
246
+
247
+
248
+ class RouteByType(DataProcessingOperator):
249
+ def __init__(self, operator_map):
250
+ self.operator_map = operator_map
251
+
252
+ def __call__(self, data):
253
+ for dtype, operator in self.operator_map:
254
+ if dtype is None or isinstance(data, dtype):
255
+ return operator(data)
256
+ raise ValueError(f"Unsupported data: {data}")
257
+
258
+
259
+ class LoadTorchPickle(DataProcessingOperator):
260
+ def __init__(self, map_location="cpu"):
261
+ self.map_location = map_location
262
+
263
+ def __call__(self, data):
264
+ return torch.load(data, map_location=self.map_location, weights_only=False)
265
+
266
+
267
+ class ToAbsolutePath(DataProcessingOperator):
268
+ def __init__(self, base_path=""):
269
+ self.base_path = base_path
270
+
271
+ def __call__(self, data):
272
+ # A remote URL is already absolute; joining it with base_path would corrupt it.
273
+ if _is_url(data):
274
+ return data
275
+ return os.path.join(self.base_path, data)
276
+
277
+
278
+ class LoadAudio(DataProcessingOperator):
279
+ def __init__(self, sr=16000):
280
+ self.sr = sr
281
+ import librosa
282
+ self.audio_loader = librosa.load
283
+
284
+ def __call__(self, data: str):
285
+ input_audio, sample_rate = self.audio_loader(data, sr=self.sr)
286
+ return input_audio
287
+
288
+
289
+ class LoadAudioWithTorchaudio(DataProcessingOperator, FrameSamplerByRateMixin):
290
+
291
+ def __init__(self, num_frames=121, time_division_factor=8, time_division_remainder=1, frame_rate=24, fix_frame_rate=True):
292
+ FrameSamplerByRateMixin.__init__(self, num_frames, time_division_factor, time_division_remainder, frame_rate, fix_frame_rate)
293
+
294
+ def __call__(self, data: str):
295
+ try:
296
+ reader = self.get_reader(data)
297
+ num_frames = self.get_num_frames(reader)
298
+ duration = num_frames / self.frame_rate
299
+ reader.close()
300
+ from ...utils.data.audio import read_audio
301
+ waveform, sample_rate = read_audio(data, duration=duration)
302
+ target_samples = int(duration * sample_rate)
303
+ current_samples = waveform.shape[-1]
304
+ if current_samples > target_samples:
305
+ waveform = waveform[..., :target_samples]
306
+ elif current_samples < target_samples:
307
+ padding = target_samples - current_samples
308
+ waveform = torch.nn.functional.pad(waveform, (0, padding))
309
+ return waveform, sample_rate
310
+ except Exception as e:
311
+ warnings.warn(f"Cannot load audio in {data} due to {e}. The audio will be `None`.")
312
+ return None
313
+
314
+
315
+ class LoadPureAudioWithTorchaudio(DataProcessingOperator):
316
+
317
+ def __init__(self, target_sample_rate=None, max_audio_duration=None, padding=False, channels=2):
318
+ self.target_sample_rate = target_sample_rate
319
+ self.max_audio_duration = max_audio_duration
320
+ self.resample = True if target_sample_rate is not None else False
321
+ self.padding = padding
322
+ from diffsynth.utils.data.audio import read_audio
323
+ self.audio_loader = read_audio
324
+
325
+ def __call__(self, data: str):
326
+ try:
327
+ waveform, sample_rate = self.audio_loader(data, resample=self.resample, resample_rate=self.target_sample_rate)
328
+ if self.max_audio_duration is not None:
329
+ target_samples = int(self.max_audio_duration * sample_rate)
330
+ current_samples = waveform.shape[-1]
331
+ if current_samples > target_samples:
332
+ waveform = waveform[..., :target_samples]
333
+ elif current_samples < target_samples and self.padding:
334
+ padding = target_samples - current_samples
335
+ waveform = torch.nn.functional.pad(waveform, (0, padding))
336
+ if waveform.shape[0] == 1:
337
+ waveform = repeat(waveform, "C L -> (N C) L", N=2)
338
+ return waveform, sample_rate
339
+ except Exception as e:
340
+ print(f"Cannot load audio in {data} due to {e}. The audio will be `None`.")
341
+ return None
diffsynth/core/data/unified_dataset.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .operators import *
2
+ import torch, json, pandas
3
+
4
+
5
+ class UnifiedDataset(torch.utils.data.Dataset):
6
+ def __init__(
7
+ self,
8
+ base_path=None, metadata_path=None,
9
+ repeat=1,
10
+ data_file_keys=tuple(),
11
+ main_data_operator=lambda x: x,
12
+ special_operator_map=None,
13
+ max_data_items=None,
14
+ cached_data_file_list=None,
15
+ ):
16
+ self.base_path = base_path
17
+ self.metadata_path = metadata_path
18
+ self.repeat = repeat
19
+ self.data_file_keys = data_file_keys
20
+ self.main_data_operator = main_data_operator
21
+ self.cached_data_operator = LoadTorchPickle()
22
+ self.special_operator_map = {} if special_operator_map is None else special_operator_map
23
+ self.max_data_items = max_data_items
24
+ self.data = []
25
+ self.cached_data = []
26
+ self.load_from_cache = metadata_path is None
27
+ self.load_metadata(metadata_path, cached_data_file_list=cached_data_file_list)
28
+
29
+ @staticmethod
30
+ def default_image_operator(
31
+ base_path="",
32
+ max_pixels=1920*1080, height=None, width=None,
33
+ height_division_factor=16, width_division_factor=16,
34
+ ):
35
+ return RouteByType(operator_map=[
36
+ (str, ToAbsolutePath(base_path) >> LoadImage() >> ImageCropAndResize(height, width, max_pixels, height_division_factor, width_division_factor)),
37
+ (list, SequencialProcess(ToAbsolutePath(base_path) >> LoadImage() >> ImageCropAndResize(height, width, max_pixels, height_division_factor, width_division_factor))),
38
+ ])
39
+
40
+ @staticmethod
41
+ def default_video_operator(
42
+ base_path="",
43
+ max_pixels=1920*1080, height=None, width=None,
44
+ height_division_factor=16, width_division_factor=16,
45
+ num_frames=81, time_division_factor=4, time_division_remainder=1,
46
+ frame_rate=24, fix_frame_rate=False,
47
+ ):
48
+ return RouteByType(operator_map=[
49
+ (str, ToAbsolutePath(base_path) >> RouteByExtensionName(operator_map=[
50
+ (("jpg", "jpeg", "png", "webp", "bmp"), LoadImage() >> ImageCropAndResize(height, width, max_pixels, height_division_factor, width_division_factor) >> ToList()),
51
+ (("gif",), LoadGIF(
52
+ num_frames, time_division_factor, time_division_remainder,
53
+ frame_processor=ImageCropAndResize(height, width, max_pixels, height_division_factor, width_division_factor),
54
+ )),
55
+ (("mp4", "avi", "mov", "wmv", "mkv", "flv", "webm"), LoadVideo(
56
+ num_frames, time_division_factor, time_division_remainder,
57
+ frame_processor=ImageCropAndResize(height, width, max_pixels, height_division_factor, width_division_factor),
58
+ frame_rate=frame_rate, fix_frame_rate=fix_frame_rate,
59
+ )),
60
+ ])),
61
+ ])
62
+
63
+ def search_for_cached_data_files(self, path):
64
+ for file_name in os.listdir(path):
65
+ subpath = os.path.join(path, file_name)
66
+ if os.path.isdir(subpath):
67
+ self.search_for_cached_data_files(subpath)
68
+ elif subpath.endswith(".pth"):
69
+ self.cached_data.append(subpath)
70
+
71
+ def load_cached_data_file_list(self, file_list_path):
72
+ with open(file_list_path, "r", encoding="utf-8") as file_list:
73
+ for line_number, line in enumerate(file_list, 1):
74
+ path = line.strip()
75
+ if not path:
76
+ continue
77
+ if not os.path.isabs(path):
78
+ path = os.path.join(self.base_path, path)
79
+ path = os.path.abspath(path)
80
+ if not path.endswith(".pth"):
81
+ raise ValueError(f"{file_list_path}:{line_number}: cached file must end in .pth: {path}")
82
+ if not os.path.isfile(path):
83
+ raise FileNotFoundError(f"{file_list_path}:{line_number}: cached file does not exist: {path}")
84
+ self.cached_data.append(path)
85
+
86
+ def load_metadata(self, metadata_path, cached_data_file_list=None):
87
+ if cached_data_file_list is not None:
88
+ if metadata_path is not None:
89
+ raise ValueError("cached_data_file_list and metadata_path are mutually exclusive")
90
+ print(f"Loading cached data paths from `{cached_data_file_list}`.")
91
+ self.load_cached_data_file_list(cached_data_file_list)
92
+ print(f"{len(self.cached_data)} listed cached data files found.")
93
+ elif metadata_path is None:
94
+ print("No metadata_path. Searching for cached data files.")
95
+ self.search_for_cached_data_files(self.base_path)
96
+ print(f"{len(self.cached_data)} cached data files found.")
97
+ elif metadata_path.endswith(".json"):
98
+ with open(metadata_path, "r") as f:
99
+ metadata = json.load(f)
100
+ self.data = metadata
101
+ elif metadata_path.endswith(".jsonl"):
102
+ metadata = []
103
+ with open(metadata_path, 'r') as f:
104
+ for line in f:
105
+ metadata.append(json.loads(line.strip()))
106
+ self.data = metadata
107
+ else:
108
+ metadata = pandas.read_csv(metadata_path)
109
+ self.data = [metadata.iloc[i].to_dict() for i in range(len(metadata))]
110
+
111
+ def __getitem__(self, data_id):
112
+ if self.load_from_cache:
113
+ data = self.cached_data[data_id % len(self.cached_data)]
114
+ data = self.cached_data_operator(data)
115
+ else:
116
+ data = self.data[data_id % len(self.data)].copy()
117
+ for key in self.data_file_keys:
118
+ if key in data:
119
+ if key in self.special_operator_map:
120
+ data[key] = self.special_operator_map[key](data[key])
121
+ elif key in self.data_file_keys:
122
+ data[key] = self.main_data_operator(data[key])
123
+ return data
124
+
125
+ def __len__(self):
126
+ if self.max_data_items is not None:
127
+ return self.max_data_items
128
+ elif self.load_from_cache:
129
+ return len(self.cached_data) * self.repeat
130
+ else:
131
+ return len(self.data) * self.repeat
132
+
133
+ def check_data_equal(self, data1, data2):
134
+ # Debug only
135
+ if len(data1) != len(data2):
136
+ return False
137
+ for k in data1:
138
+ if data1[k] != data2[k]:
139
+ return False
140
+ return True
diffsynth/core/device/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .npu_compatible_device import parse_device_type, parse_nccl_backend, get_available_device_type, get_device_name
2
+ from .npu_compatible_device import IS_NPU_AVAILABLE, IS_CUDA_AVAILABLE
diffsynth/core/device/npu_compatible_device.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+ import torch
3
+ from typing import Any
4
+
5
+
6
+ def is_torch_npu_available():
7
+ return importlib.util.find_spec("torch_npu") is not None
8
+
9
+
10
+ IS_CUDA_AVAILABLE = torch.cuda.is_available()
11
+ IS_NPU_AVAILABLE = is_torch_npu_available() and torch.npu.is_available()
12
+
13
+ if IS_NPU_AVAILABLE:
14
+ import torch_npu
15
+
16
+ torch.npu.config.allow_internal_format = False
17
+
18
+
19
+ def get_device_type() -> str:
20
+ """Get device type based on current machine, currently only support CPU, CUDA, NPU."""
21
+ if IS_CUDA_AVAILABLE:
22
+ device = "cuda"
23
+ elif IS_NPU_AVAILABLE:
24
+ device = "npu"
25
+ else:
26
+ device = "cpu"
27
+
28
+ return device
29
+
30
+
31
+ def get_torch_device() -> Any:
32
+ """Get torch attribute based on device type, e.g. torch.cuda or torch.npu"""
33
+ device_name = get_device_type()
34
+
35
+ try:
36
+ return getattr(torch, device_name)
37
+ except AttributeError:
38
+ print(f"Device namespace '{device_name}' not found in torch, try to load 'torch.cuda'.")
39
+ return torch.cuda
40
+
41
+
42
+ def get_device_id() -> int:
43
+ """Get current device id based on device type."""
44
+ return get_torch_device().current_device()
45
+
46
+
47
+ def get_device_name() -> str:
48
+ """Get current device name based on device type."""
49
+ return f"{get_device_type()}:{get_device_id()}"
50
+
51
+
52
+ def synchronize() -> None:
53
+ """Execute torch synchronize operation."""
54
+ get_torch_device().synchronize()
55
+
56
+
57
+ def empty_cache() -> None:
58
+ """Execute torch empty cache operation."""
59
+ get_torch_device().empty_cache()
60
+
61
+
62
+ def get_nccl_backend() -> str:
63
+ """Return distributed communication backend type based on device type."""
64
+ if IS_CUDA_AVAILABLE:
65
+ return "nccl"
66
+ elif IS_NPU_AVAILABLE:
67
+ return "hccl"
68
+ else:
69
+ raise RuntimeError(f"No available distributed communication backend found on device type {get_device_type()}.")
70
+
71
+
72
+ def enable_high_precision_for_bf16():
73
+ """
74
+ Set high accumulation dtype for matmul and reduction.
75
+ """
76
+ if IS_CUDA_AVAILABLE:
77
+ torch.backends.cuda.matmul.allow_tf32 = False
78
+ torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction = False
79
+
80
+ if IS_NPU_AVAILABLE:
81
+ torch.npu.matmul.allow_tf32 = False
82
+ torch.npu.matmul.allow_bf16_reduced_precision_reduction = False
83
+
84
+
85
+ def parse_device_type(device):
86
+ if isinstance(device, torch.device):
87
+ return device.type
88
+ elif isinstance(device, str):
89
+ # `torch.device("npu")` raises when torch_npu is not installed, so keep npu ahead of the generic parse.
90
+ if device.startswith("npu"):
91
+ return "npu"
92
+ try:
93
+ return torch.device(device).type
94
+ except RuntimeError:
95
+ # Strings that are not device names at all
96
+ return "cpu"
97
+
98
+
99
+ def parse_nccl_backend(device_type):
100
+ if device_type == "cuda":
101
+ return "nccl"
102
+ elif device_type == "npu":
103
+ return "hccl"
104
+ else:
105
+ raise RuntimeError(f"No available distributed communication backend found on device type {device_type}.")
106
+
107
+
108
+ def get_available_device_type():
109
+ return get_device_type()
diffsynth/core/gradient/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .gradient_checkpoint import gradient_checkpoint_forward
diffsynth/core/gradient/gradient_checkpoint.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+
4
+ try:
5
+ import deepspeed
6
+ _HAS_DEEPSPEED = True
7
+ except ModuleNotFoundError:
8
+ _HAS_DEEPSPEED = False
9
+
10
+
11
+ def create_custom_forward(module):
12
+ def custom_forward(*inputs, **kwargs):
13
+ return module(*inputs, **kwargs)
14
+ return custom_forward
15
+
16
+
17
+ def create_custom_forward_use_reentrant(module):
18
+ def custom_forward(*inputs):
19
+ return module(*inputs)
20
+ return custom_forward
21
+
22
+
23
+ def judge_args_requires_grad(*args):
24
+ for arg in args:
25
+ if isinstance(arg, torch.Tensor) and arg.requires_grad:
26
+ return True
27
+ return False
28
+
29
+
30
+ def gradient_checkpoint_forward(
31
+ model,
32
+ use_gradient_checkpointing,
33
+ use_gradient_checkpointing_offload,
34
+ *args,
35
+ **kwargs,
36
+ ):
37
+ if use_gradient_checkpointing and _HAS_DEEPSPEED and deepspeed.checkpointing.is_configured():
38
+ all_args = args + tuple(kwargs.values())
39
+ if not judge_args_requires_grad(*all_args):
40
+ # get the first grad_enabled tensor from un_checkpointed forward
41
+ model_output = model(*args, **kwargs)
42
+ else:
43
+ model_output = deepspeed.checkpointing.checkpoint(
44
+ create_custom_forward_use_reentrant(model),
45
+ *all_args,
46
+ )
47
+ return model_output
48
+ if use_gradient_checkpointing_offload:
49
+ with torch.autograd.graph.save_on_cpu():
50
+ model_output = torch.utils.checkpoint.checkpoint(
51
+ create_custom_forward(model),
52
+ *args,
53
+ **kwargs,
54
+ use_reentrant=False,
55
+ )
56
+ elif use_gradient_checkpointing:
57
+ model_output = torch.utils.checkpoint.checkpoint(
58
+ create_custom_forward(model),
59
+ *args,
60
+ **kwargs,
61
+ use_reentrant=False,
62
+ )
63
+ else:
64
+ model_output = model(*args, **kwargs)
65
+ return model_output
diffsynth/core/loader/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .file import load_state_dict, hash_state_dict_keys, hash_model_file
2
+ from .model import load_model, load_model_with_disk_offload
3
+ from .config import ModelConfig
diffsynth/core/loader/config.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch, glob, os, sys
2
+ from typing import Optional, Union, Dict
3
+ from dataclasses import dataclass
4
+ from modelscope import snapshot_download
5
+ from huggingface_hub import snapshot_download as hf_snapshot_download
6
+ from typing import Optional
7
+ from ..quant import QuantizeConfig, MixedQuantizeConfig
8
+
9
+ _download_tips_printed = False
10
+
11
+ if sys.platform.startswith('win'):
12
+ DOWNLOADING_TIPS = """
13
+ ┌──────────────────────────────────────────────────────────────────────┐
14
+ │ DiffSynth-Studio Model Downloader Configuration: │
15
+ │ │
16
+ │ [0] Download from https://modelscope.cn/ │
17
+ │ (default behavior) │
18
+ │ [1] Download from https://modelscope.ai/ │
19
+ │ (enabled via `$env:MODELSCOPE_ENDPOINT="https://modelscope.ai"`) │
20
+ │ [2] Download from https://huggingface.co/ │
21
+ │ (enabled via `$env:DIFFSYNTH_DOWNLOAD_SOURCE="HuggingFace"`) │
22
+ │ [3] Skip download and load only pre-downloaded model files │
23
+ │ (enabled via `$env:DIFFSYNTH_SKIP_DOWNLOAD="True"`) │
24
+ └──────────────────────────────────────────────────────────────────────┘
25
+ """.strip()
26
+ else:
27
+ DOWNLOADING_TIPS = """
28
+ ┌──────────────────────────────────────────────────────────────────────┐
29
+ │ DiffSynth-Studio Model Downloader Configuration: │
30
+ │ │
31
+ │ [0] Download from https://modelscope.cn/ │
32
+ │ (default behavior) │
33
+ │ [1] Download from https://modelscope.ai/ │
34
+ │ (enabled via `export MODELSCOPE_ENDPOINT=https://modelscope.ai`) │
35
+ │ [2] Download from https://huggingface.co/ │
36
+ │ (enabled via `export DIFFSYNTH_DOWNLOAD_SOURCE=HuggingFace`) │
37
+ │ [3] Skip download and load only pre-downloaded model files │
38
+ │ (enabled via `export DIFFSYNTH_SKIP_DOWNLOAD=True`) │
39
+ └──────────────────────────────────────────────────────────────────────┘
40
+ """.strip()
41
+
42
+
43
+ @dataclass
44
+ class ModelConfig:
45
+ path: Union[str, list[str]] = None
46
+ model_id: str = None
47
+ origin_file_pattern: Union[str, list[str]] = None
48
+ download_source: str = None
49
+ local_model_path: str = None
50
+ skip_download: bool = None
51
+ offload_device: Optional[Union[str, torch.device]] = None
52
+ offload_dtype: Optional[torch.dtype] = None
53
+ onload_device: Optional[Union[str, torch.device]] = None
54
+ onload_dtype: Optional[torch.dtype] = None
55
+ preparing_device: Optional[Union[str, torch.device]] = None
56
+ preparing_dtype: Optional[torch.dtype] = None
57
+ computation_device: Optional[Union[str, torch.device]] = None
58
+ computation_dtype: Optional[torch.dtype] = None
59
+ clear_parameters: bool = False
60
+ state_dict: Dict[str, torch.Tensor] = None
61
+ quantize: Optional[Union[QuantizeConfig, MixedQuantizeConfig]] = None
62
+
63
+ def check_input(self):
64
+ if self.path is None and self.model_id is None:
65
+ raise ValueError(f"""No valid model files. Please use `ModelConfig(path="xxx")` or `ModelConfig(model_id="xxx/yyy", origin_file_pattern="zzz")`. `skip_download=True` only supports the first one.""")
66
+
67
+ def parse_original_file_pattern(self):
68
+ if self.origin_file_pattern in [None, "", "./"]:
69
+ return "*"
70
+ elif self.origin_file_pattern.endswith("/"):
71
+ return self.origin_file_pattern + "*"
72
+ else:
73
+ return self.origin_file_pattern
74
+
75
+ def parse_download_source(self):
76
+ if self.download_source is None:
77
+ if os.environ.get('DIFFSYNTH_DOWNLOAD_SOURCE') is not None:
78
+ return os.environ.get('DIFFSYNTH_DOWNLOAD_SOURCE').lower()
79
+ else:
80
+ return "modelscope"
81
+ else:
82
+ return self.download_source.lower()
83
+
84
+ def parse_skip_download(self):
85
+ if self.skip_download is None:
86
+ if os.environ.get('DIFFSYNTH_SKIP_DOWNLOAD') is not None:
87
+ if os.environ.get('DIFFSYNTH_SKIP_DOWNLOAD').lower() == "true":
88
+ return True
89
+ elif os.environ.get('DIFFSYNTH_SKIP_DOWNLOAD').lower() == "false":
90
+ return False
91
+ else:
92
+ return False
93
+ else:
94
+ return self.skip_download
95
+
96
+ def download(self):
97
+ origin_file_pattern = self.parse_original_file_pattern()
98
+ downloaded_files = glob.glob(origin_file_pattern, root_dir=os.path.join(self.local_model_path, self.model_id))
99
+ download_source = self.parse_download_source()
100
+ if download_source.lower() == "modelscope":
101
+ snapshot_download(
102
+ self.model_id,
103
+ local_dir=os.path.join(self.local_model_path, self.model_id),
104
+ allow_file_pattern=origin_file_pattern,
105
+ ignore_file_pattern=downloaded_files,
106
+ local_files_only=False
107
+ )
108
+ elif download_source.lower() == "huggingface":
109
+ hf_snapshot_download(
110
+ self.model_id,
111
+ local_dir=os.path.join(self.local_model_path, self.model_id),
112
+ allow_patterns=origin_file_pattern,
113
+ ignore_patterns=downloaded_files,
114
+ local_files_only=False
115
+ )
116
+ else:
117
+ raise ValueError("`download_source` should be `modelscope` or `huggingface`.")
118
+
119
+ def require_downloading(self):
120
+ if self.path is not None:
121
+ return False
122
+ skip_download = self.parse_skip_download()
123
+ return not skip_download
124
+
125
+ def reset_local_model_path(self):
126
+ if os.environ.get('DIFFSYNTH_MODEL_BASE_PATH') is not None:
127
+ self.local_model_path = os.environ.get('DIFFSYNTH_MODEL_BASE_PATH')
128
+ elif self.local_model_path is None:
129
+ self.local_model_path = "./models"
130
+
131
+ def check_download_source(self):
132
+ download_source = self.parse_download_source().lower()
133
+ if os.environ.get('DIFFSYNTH_SKIP_DOWNLOAD', "").lower() == "true":
134
+ behavior = 3
135
+ elif download_source == "modelscope":
136
+ if "modelscope.ai" in os.environ.get("MODELSCOPE_ENDPOINT", "") or "modelscope.ai" in os.environ.get("MODELSCOPE_DOMAIN", ""):
137
+ behavior = 1
138
+ else:
139
+ behavior = 0
140
+ else:
141
+ behavior = 2
142
+ tips = DOWNLOADING_TIPS
143
+ for i in range(4): tips = tips.replace(f"[{i}]", ["[ ]", "[√]"][i==behavior])
144
+ global _download_tips_printed
145
+ if not _download_tips_printed:
146
+ is_main_process = True
147
+ if torch.distributed.is_initialized():
148
+ is_main_process = torch.distributed.get_rank() == 0
149
+ if is_main_process:
150
+ print(tips)
151
+ _download_tips_printed = True
152
+
153
+ def download_if_necessary(self):
154
+ self.check_download_source()
155
+ self.check_input()
156
+ self.reset_local_model_path()
157
+ if self.require_downloading():
158
+ self.download()
159
+ if self.path is None:
160
+ if self.origin_file_pattern in [None, "", "./"]:
161
+ self.path = os.path.join(self.local_model_path, self.model_id)
162
+ else:
163
+ self.path = glob.glob(os.path.join(self.local_model_path, self.model_id, self.origin_file_pattern))
164
+ if isinstance(self.path, list) and len(self.path) == 1:
165
+ self.path = self.path[0]
166
+
167
+ def vram_config(self):
168
+ return {
169
+ "offload_device": self.offload_device,
170
+ "offload_dtype": self.offload_dtype,
171
+ "onload_device": self.onload_device,
172
+ "onload_dtype": self.onload_dtype,
173
+ "preparing_device": self.preparing_device,
174
+ "preparing_dtype": self.preparing_dtype,
175
+ "computation_device": self.computation_device,
176
+ "computation_dtype": self.computation_dtype,
177
+ }
diffsynth/core/loader/file.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from safetensors import safe_open
2
+ import torch, hashlib, json
3
+
4
+
5
+ def load_state_dict(file_path, torch_dtype=None, device="cpu", pin_memory=False, verbose=0):
6
+ if isinstance(file_path, list):
7
+ state_dict = {}
8
+ for file_path_ in file_path:
9
+ state_dict.update(load_state_dict(file_path_, torch_dtype, device, pin_memory=pin_memory, verbose=verbose))
10
+ else:
11
+ if verbose >= 1:
12
+ print(f"Loading file [started]: {file_path}")
13
+ if file_path.endswith(".safetensors"):
14
+ state_dict = load_state_dict_from_safetensors(file_path, torch_dtype=torch_dtype, device=device)
15
+ else:
16
+ state_dict = load_state_dict_from_bin(file_path, torch_dtype=torch_dtype, device=device)
17
+ # If load state dict in CPU memory, `pin_memory=True` will make `model.to("cuda")` faster.
18
+ if pin_memory:
19
+ for i in state_dict:
20
+ state_dict[i] = state_dict[i].pin_memory()
21
+ if verbose >= 1:
22
+ print(f"Loading file [done]: {file_path}")
23
+ return state_dict
24
+
25
+
26
+ def load_state_dict_from_safetensors(file_path, torch_dtype=None, device="cpu"):
27
+ state_dict = {}
28
+ with safe_open(file_path, framework="pt", device=str(device)) as f:
29
+ for k in f.keys():
30
+ state_dict[k] = f.get_tensor(k)
31
+ if torch_dtype is not None:
32
+ state_dict[k] = state_dict[k].to(torch_dtype)
33
+ return state_dict
34
+
35
+
36
+ def load_metadata_from_safetensors(file_path):
37
+ if isinstance(file_path, list):
38
+ metadata = {}
39
+ for file_path_ in file_path:
40
+ for key, value in load_metadata_from_safetensors(file_path_).items():
41
+ metadata[key] = merge_metadata_value(metadata[key], value) if key in metadata else value
42
+ return metadata
43
+ with safe_open(file_path, framework="pt", device="cpu") as f:
44
+ return f.metadata() or {}
45
+
46
+
47
+ def merge_metadata_value(old_value, new_value):
48
+ try:
49
+ old_items, new_items = json.loads(old_value), json.loads(new_value)
50
+ except (json.JSONDecodeError, TypeError):
51
+ return new_value
52
+ if isinstance(old_items, list) and isinstance(new_items, list):
53
+ return json.dumps(old_items + [i for i in new_items if i not in old_items])
54
+ return new_value
55
+
56
+
57
+ def load_state_dict_from_bin(file_path, torch_dtype=None, device="cpu"):
58
+ state_dict = torch.load(file_path, map_location=device, weights_only=True)
59
+ if len(state_dict) == 1:
60
+ if "state_dict" in state_dict:
61
+ state_dict = state_dict["state_dict"]
62
+ elif "module" in state_dict:
63
+ state_dict = state_dict["module"]
64
+ elif "model_state" in state_dict:
65
+ state_dict = state_dict["model_state"]
66
+ if torch_dtype is not None:
67
+ for i in state_dict:
68
+ if isinstance(state_dict[i], torch.Tensor):
69
+ state_dict[i] = state_dict[i].to(torch_dtype)
70
+ return state_dict
71
+
72
+
73
+ def convert_state_dict_keys_to_single_str(state_dict, with_shape=True):
74
+ keys = []
75
+ for key, value in state_dict.items():
76
+ if isinstance(key, str):
77
+ if isinstance(value, torch.Tensor):
78
+ if with_shape:
79
+ shape = "_".join(map(str, list(value.shape)))
80
+ keys.append(key + ":" + shape)
81
+ keys.append(key)
82
+ elif isinstance(value, dict):
83
+ keys.append(key + "|" + convert_state_dict_keys_to_single_str(value, with_shape=with_shape))
84
+ keys.sort()
85
+ keys_str = ",".join(keys)
86
+ return keys_str
87
+
88
+
89
+ def hash_state_dict_keys(state_dict, with_shape=True):
90
+ keys_str = convert_state_dict_keys_to_single_str(state_dict, with_shape=with_shape)
91
+ keys_str = keys_str.encode(encoding="UTF-8")
92
+ return hashlib.md5(keys_str).hexdigest()
93
+
94
+
95
+ def load_keys_dict(file_path):
96
+ if isinstance(file_path, list):
97
+ state_dict = {}
98
+ for file_path_ in file_path:
99
+ state_dict.update(load_keys_dict(file_path_))
100
+ return state_dict
101
+ if file_path.endswith(".safetensors"):
102
+ return load_keys_dict_from_safetensors(file_path)
103
+ else:
104
+ return load_keys_dict_from_bin(file_path)
105
+
106
+
107
+ def load_keys_dict_from_safetensors(file_path):
108
+ keys_dict = {}
109
+ with safe_open(file_path, framework="pt", device="cpu") as f:
110
+ for k in f.keys():
111
+ keys_dict[k] = f.get_slice(k).get_shape()
112
+ return keys_dict
113
+
114
+
115
+ def convert_state_dict_to_keys_dict(state_dict):
116
+ keys_dict = {}
117
+ for k, v in state_dict.items():
118
+ if isinstance(v, torch.Tensor):
119
+ keys_dict[k] = list(v.shape)
120
+ else:
121
+ keys_dict[k] = convert_state_dict_to_keys_dict(v)
122
+ return keys_dict
123
+
124
+
125
+ def load_keys_dict_from_bin(file_path):
126
+ state_dict = load_state_dict_from_bin(file_path)
127
+ keys_dict = convert_state_dict_to_keys_dict(state_dict)
128
+ return keys_dict
129
+
130
+
131
+ def convert_keys_dict_to_single_str(state_dict, with_shape=True):
132
+ keys = []
133
+ for key, value in state_dict.items():
134
+ if isinstance(key, str):
135
+ if isinstance(value, dict):
136
+ keys.append(key + "|" + convert_keys_dict_to_single_str(value, with_shape=with_shape))
137
+ else:
138
+ if with_shape:
139
+ shape = "_".join(map(str, list(value)))
140
+ keys.append(key + ":" + shape)
141
+ keys.append(key)
142
+ keys.sort()
143
+ keys_str = ",".join(keys)
144
+ return keys_str
145
+
146
+
147
+ def hash_model_file(path, with_shape=True):
148
+ keys_dict = load_keys_dict(path)
149
+ keys_str = convert_keys_dict_to_single_str(keys_dict, with_shape=with_shape)
150
+ keys_str = keys_str.encode(encoding="UTF-8")
151
+ return hashlib.md5(keys_str).hexdigest()
diffsynth/core/loader/model.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ..vram.initialization import skip_model_initialization
2
+ from ..vram.disk_map import DiskMap
3
+ from ..vram.layers import enable_vram_management
4
+ from .file import load_state_dict, load_metadata_from_safetensors
5
+ import torch
6
+ from contextlib import contextmanager
7
+ from transformers.integrations import is_deepspeed_zero3_enabled
8
+ from transformers.utils import ContextManagers
9
+
10
+
11
+ def load_model(model_class, path, config=None, torch_dtype=torch.bfloat16, device="cpu", state_dict_converter=None, use_disk_map=False, module_map=None, vram_config=None, vram_limit=None, state_dict=None, quantize=None):
12
+ config = {} if config is None else config
13
+ with ContextManagers(get_init_context(torch_dtype=torch_dtype, device=device)):
14
+ model = model_class(**config)
15
+ # What is `module_map`?
16
+ # This is a module mapping table for VRAM management.
17
+ if module_map is not None and quantize is None:
18
+ devices = [vram_config["offload_device"], vram_config["onload_device"], vram_config["preparing_device"], vram_config["computation_device"]]
19
+ device = [d for d in devices if d != "disk"][0]
20
+ dtypes = [vram_config["offload_dtype"], vram_config["onload_dtype"], vram_config["preparing_dtype"], vram_config["computation_dtype"]]
21
+ dtype = [d for d in dtypes if d != "disk"][0]
22
+ if vram_config["offload_device"] != "disk":
23
+ if state_dict is None: state_dict = DiskMap(path, device, torch_dtype=dtype)
24
+ if state_dict_converter is not None:
25
+ state_dict = state_dict_converter(state_dict)
26
+ else:
27
+ state_dict = {i: state_dict[i] for i in state_dict}
28
+ model.load_state_dict(state_dict, assign=True)
29
+ model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=None, vram_limit=vram_limit)
30
+ else:
31
+ disk_map = DiskMap(path, device, state_dict_converter=state_dict_converter)
32
+ model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=disk_map, vram_limit=vram_limit)
33
+ elif quantize is not None and module_map is not None:
34
+ if "disk" in vram_config.values():
35
+ if not quantize.load_prequantized:
36
+ raise ValueError("Disk offload with quantization is only supported for pre-quantized checkpoints (load_prequantized=True).")
37
+ devices = [vram_config[k] for k in ("offload_device", "onload_device", "preparing_device", "computation_device")]
38
+ load_device = [d for d in devices if d != "disk"][0]
39
+ disk_map = DiskMap(path, load_device, torch_dtype=None, state_dict_converter=state_dict_converter)
40
+ metadata = load_metadata_from_safetensors(path)
41
+ model = quantize.prepare_for_prequantized_load(model, compute_dtype=vram_config["computation_dtype"])
42
+ model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=disk_map, vram_limit=vram_limit, quantize=quantize, metadata=metadata)
43
+ else:
44
+ offload_device = vram_config["offload_device"]
45
+ computation_device = vram_config["computation_device"]
46
+ computation_dtype = vram_config["computation_dtype"]
47
+ load_dtype = None if quantize.load_prequantized else computation_dtype
48
+ if state_dict is None: state_dict = DiskMap(path, offload_device, torch_dtype=load_dtype)
49
+ if state_dict_converter is not None:
50
+ state_dict = state_dict_converter(state_dict)
51
+ else:
52
+ state_dict = {i: state_dict[i] for i in state_dict}
53
+
54
+ if quantize.load_prequantized:
55
+ model = quantize.prepare_for_prequantized_load(model, compute_dtype=computation_dtype)
56
+ state_dict = quantize.unflatten_state_dict(state_dict, load_metadata_from_safetensors(path))
57
+
58
+ model.load_state_dict(state_dict, assign=True)
59
+ state_dict = None
60
+
61
+ model = quantize.quantize_model(model, compute_device=computation_device, model_device=offload_device)
62
+ model = quantize.dequantize_model(model, compute_dtype=computation_dtype, compute_device=computation_device, model_device=offload_device)
63
+ model = model.to(dtype=computation_dtype, device=offload_device)
64
+ model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=None, vram_limit=vram_limit, quantize=quantize)
65
+ elif quantize is not None:
66
+ # Weight-only quantization (see `diffsynth.core.quant`), isolated from the normal path below.
67
+ if quantize.load_prequantized:
68
+ load_device, load_dtype = device, None
69
+ else:
70
+ load_device, load_dtype = "cpu", torch_dtype
71
+
72
+ if state_dict is not None:
73
+ pass
74
+ elif use_disk_map:
75
+ state_dict = DiskMap(path, load_device, torch_dtype=load_dtype)
76
+ else:
77
+ state_dict = load_state_dict(path, load_dtype, load_device)
78
+
79
+ if state_dict_converter is not None:
80
+ state_dict = state_dict_converter(state_dict)
81
+ else:
82
+ state_dict = {i: state_dict[i] for i in state_dict}
83
+
84
+ if quantize.load_prequantized:
85
+ model = quantize.prepare_for_prequantized_load(model, compute_dtype=torch_dtype or torch.bfloat16)
86
+ state_dict = quantize.unflatten_state_dict(state_dict, load_metadata_from_safetensors(path))
87
+
88
+ model.load_state_dict(state_dict, assign=True)
89
+ model = quantize.quantize_model(model, compute_device=device, model_device=device)
90
+ model = quantize.dequantize_model(model, compute_dtype=torch_dtype or torch.bfloat16)
91
+ model = model.to(dtype=torch_dtype, device=device)
92
+ else:
93
+ # Why do we use `DiskMap`?
94
+ # Sometimes a model file contains multiple models,
95
+ # and DiskMap can load only the parameters of a single model,
96
+ # avoiding the need to load all parameters in the file.
97
+ if state_dict is not None:
98
+ pass
99
+ elif use_disk_map:
100
+ state_dict = DiskMap(path, device, torch_dtype=torch_dtype)
101
+ else:
102
+ state_dict = load_state_dict(path, torch_dtype, device)
103
+ # Why do we use `state_dict_converter`?
104
+ # Some models are saved in complex formats,
105
+ # and we need to convert the state dict into the appropriate format.
106
+ if state_dict_converter is not None:
107
+ state_dict = state_dict_converter(state_dict)
108
+ else:
109
+ state_dict = {i: state_dict[i] for i in state_dict}
110
+ # Why does DeepSpeed ZeRO Stage 3 need to be handled separately?
111
+ # Because at this stage, model parameters are partitioned across multiple GPUs.
112
+ # Loading them directly could lead to excessive GPU memory consumption.
113
+ if is_deepspeed_zero3_enabled():
114
+ from transformers.integrations.deepspeed import _load_state_dict_into_zero3_model
115
+ _load_state_dict_into_zero3_model(model, state_dict)
116
+ else:
117
+ model.load_state_dict(state_dict, assign=True)
118
+ # Why do we call `to()`?
119
+ # Because some models override the behavior of `to()`,
120
+ # especially those from libraries like Transformers.
121
+ model = model.to(dtype=torch_dtype, device=device)
122
+ if quantize is not None:
123
+ # Downstream steps (e.g. LoRA hot-loading) need the config to handle the quantized layers.
124
+ model.quantize_config = quantize
125
+ if hasattr(model, "eval"):
126
+ model = model.eval()
127
+ return model
128
+
129
+
130
+ def load_model_with_disk_offload(model_class, path, config=None, torch_dtype=torch.bfloat16, device="cpu", state_dict_converter=None, module_map=None):
131
+ if isinstance(path, str):
132
+ path = [path]
133
+ config = {} if config is None else config
134
+ with skip_model_initialization():
135
+ model = model_class(**config)
136
+ if hasattr(model, "eval"):
137
+ model = model.eval()
138
+ disk_map = DiskMap(path, device, state_dict_converter=state_dict_converter)
139
+ vram_config = {
140
+ "offload_dtype": "disk",
141
+ "offload_device": "disk",
142
+ "onload_dtype": "disk",
143
+ "onload_device": "disk",
144
+ "preparing_dtype": torch.float8_e4m3fn,
145
+ "preparing_device": device,
146
+ "computation_dtype": torch_dtype,
147
+ "computation_device": device,
148
+ }
149
+ enable_vram_management(model, module_map, vram_config=vram_config, disk_map=disk_map, vram_limit=80)
150
+ return model
151
+
152
+
153
+ def get_init_context(torch_dtype, device):
154
+ if is_deepspeed_zero3_enabled():
155
+ from transformers.modeling_utils import set_zero3_state
156
+ import deepspeed
157
+ # Why do we use "deepspeed.zero.Init"?
158
+ # Weight segmentation of the model can be performed on the CPU side
159
+ # and loading the segmented weights onto the computing card
160
+ init_contexts = [deepspeed.zero.Init(remote_device=device, dtype=torch_dtype), set_zero3_state()]
161
+ else:
162
+ # Why do we use `skip_model_initialization`?
163
+ # It skips the random initialization of model parameters,
164
+ # thereby speeding up model loading and avoiding excessive memory usage.
165
+ init_contexts = [skip_model_initialization()]
166
+
167
+ return init_contexts
diffsynth/core/npu_patch/npu_fused_operator.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from ..device.npu_compatible_device import get_device_type
3
+ try:
4
+ import torch_npu
5
+ except:
6
+ pass
7
+
8
+
9
+ def rms_norm_forward_npu(self, hidden_states):
10
+ "npu rms fused operator for RMSNorm.forward from diffsynth\models\general_modules.py"
11
+ if hidden_states.dtype != self.weight.dtype:
12
+ hidden_states = hidden_states.to(self.weight.dtype)
13
+ return torch_npu.npu_rms_norm(hidden_states, self.weight, self.eps)[0]
14
+
15
+
16
+ def rms_norm_forward_transformers_npu(self, hidden_states):
17
+ "npu rms fused operator for transformers"
18
+ if hidden_states.dtype != self.weight.dtype:
19
+ hidden_states = hidden_states.to(self.weight.dtype)
20
+ return torch_npu.npu_rms_norm(hidden_states, self.weight, self.variance_epsilon)[0]
21
+
22
+
23
+ def rotary_emb_Zimage_npu(self, x_in: torch.Tensor, freqs_cis: torch.Tensor):
24
+ "npu rope fused operator for Zimage"
25
+ with torch.amp.autocast(get_device_type(), enabled=False):
26
+ freqs_cis = freqs_cis.unsqueeze(2)
27
+ cos, sin = torch.chunk(torch.view_as_real(freqs_cis), 2, dim=-1)
28
+ cos = cos.expand(-1, -1, -1, -1, 2).flatten(-2)
29
+ sin = sin.expand(-1, -1, -1, -1, 2).flatten(-2)
30
+ return torch_npu.npu_rotary_mul(x_in, cos, sin, rotary_mode="interleave").to(x_in)
diffsynth/core/offload_training/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .manager import OffloadTrainingManager
diffsynth/core/offload_training/manager.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Layer offloading for training — hook-based CPU offload.
3
+
4
+ Hook lifecycle per module:
5
+
6
+ No checkpointing:
7
+ forward_pre(load→GPU) → forward() → forward_hook(offload)
8
+ backward_pre(load→GPU) → backward() → backward_hook(offload)
9
+
10
+ With checkpointing (use_reentrant=False):
11
+ First forward:
12
+ forward_pre(load→GPU) → forward() → forward_hook(offload, mark in_recompute)
13
+ Recomputing forward (during backward):
14
+ forward_pre(load→GPU) → forward() → forward_hook(in_recompute=True → keep GPU)
15
+ Backward:
16
+ backward_pre(load→GPU) → backward() → backward_hook(offload)
17
+ """
18
+ import torch
19
+ import torch.nn as nn
20
+ import warnings
21
+ from .offloader import StaticParamOffloader, TrainableParamOffloader, AlwaysOnGPUParamOffloader, BufferOffloader
22
+ from .memory_buffer import PinnedArenaPool, BaseBufferPool
23
+ warnings.filterwarnings("ignore", message="Full backward hook is firing when gradients are computed with respect to module outputs")
24
+
25
+
26
+ def has_parameters(module: nn.Module) -> bool:
27
+ return len(list(module.parameters())) > 0
28
+
29
+ def count_parameters(module: nn.Module) -> int:
30
+ return sum(p.numel() for p in module.parameters())
31
+
32
+ def is_leaf_module(module: nn.Module) -> bool:
33
+ return len(list(module.children())) == 0
34
+
35
+
36
+ class UnitWiseParamManager:
37
+ def __init__(self, model: nn.Module, target_device: torch.device, enable_optimizer_cpu_offload: bool = False, params: list = None, buffers: list = None, memory_buffer: BaseBufferPool = None):
38
+ self.model = model
39
+ self.target_device = target_device
40
+ self.param_offloaders = {}
41
+ for param in (model.parameters() if params is None else params):
42
+ if not param.requires_grad:
43
+ self.param_offloaders[id(param)] = StaticParamOffloader(param, target_device, memory_buffer=memory_buffer)
44
+ else:
45
+ if enable_optimizer_cpu_offload:
46
+ self.param_offloaders[id(param)] = TrainableParamOffloader(param, target_device)
47
+ else:
48
+ self.param_offloaders[id(param)] = AlwaysOnGPUParamOffloader(param, target_device)
49
+ if buffers is not None and len(buffers) > 0:
50
+ for mod, buf_name, buf in buffers:
51
+ self.param_offloaders[id(buf)] = BufferOffloader(mod, buf_name, buf, target_device, memory_buffer=memory_buffer)
52
+
53
+ def move_gradients_to_cpu(self):
54
+ for offloader in self.param_offloaders.values():
55
+ offloader.offload_grad()
56
+
57
+ def onload_module(self, module: nn.Module):
58
+ for param in module.parameters(recurse=False):
59
+ if id(param) in self.param_offloaders:
60
+ self.param_offloaders[id(param)].onload()
61
+ for name, buf in module.named_buffers(recurse=False):
62
+ if id(buf) in self.param_offloaders:
63
+ self.param_offloaders[id(buf)].onload()
64
+
65
+ def offload_module(self, module: nn.Module):
66
+ for param in module.parameters(recurse=False):
67
+ if id(param) in self.param_offloaders:
68
+ self.param_offloaders[id(param)].offload()
69
+ for name, buf in module.named_buffers(recurse=False):
70
+ if id(buf) in self.param_offloaders:
71
+ self.param_offloaders[id(buf)].offload()
72
+
73
+
74
+ class UnitWiseHookManager:
75
+ def __init__(self, model: nn.Module, target_device: torch.device, enable_optimizer_cpu_offload: bool = False,
76
+ params: list = None, buffers: list = None, memory_buffer: BaseBufferPool = None):
77
+ self.param_manager = UnitWiseParamManager(model, target_device, enable_optimizer_cpu_offload, params=params, buffers=buffers, memory_buffer=memory_buffer)
78
+ self._in_recompute: set = set()
79
+ self._register_hooks(model)
80
+
81
+ def _register_hooks(self, module: nn.Module):
82
+ def forward_pre_hook(mod, args):
83
+ self.param_manager.onload_module(mod)
84
+
85
+ def forward_hook(mod, args, output):
86
+ if mod in self._in_recompute:
87
+ return
88
+ self._in_recompute.add(mod)
89
+ self.param_manager.offload_module(mod)
90
+
91
+ def backward_pre_hook(mod, grad_output):
92
+ self.param_manager.onload_module(mod)
93
+
94
+ def backward_hook(mod, grad_input, grad_output):
95
+ self.param_manager.offload_module(mod)
96
+
97
+ module.register_forward_pre_hook(forward_pre_hook)
98
+ module.register_forward_hook(forward_hook)
99
+ module.register_full_backward_pre_hook(backward_pre_hook)
100
+ if is_leaf_module(module):
101
+ module.register_full_backward_hook(backward_hook)
102
+ else:
103
+ # Parent module backward_hook fires before child backward completes.
104
+ # Register on leaf children instead.
105
+ sub_modules = [m for m in module.modules() if is_leaf_module(m) and has_parameters(m)]
106
+ for sub_mod in sub_modules:
107
+ sub_mod.register_full_backward_hook(backward_hook)
108
+
109
+ def after_backward(self):
110
+ self._in_recompute.clear()
111
+ self.param_manager.move_gradients_to_cpu()
112
+
113
+ @property
114
+ def managed_param_ids(self):
115
+ return set(self.param_manager.param_offloaders.keys())
116
+
117
+
118
+ class OffloadTrainingManager:
119
+ def __init__(self, model: nn.Module, target_device: torch.device, enable_optimizer_cpu_offload: bool = False, cpu_offload_split_threshold: int = None):
120
+ self.model = model
121
+ self.target_device = target_device
122
+ self.enable_optimizer_cpu_offload = enable_optimizer_cpu_offload
123
+ cpu_offload_split_threshold = cpu_offload_split_threshold * 1024 * 1024 if cpu_offload_split_threshold is not None else None
124
+ self._register_units(model, target_device, enable_optimizer_cpu_offload, cpu_offload_split_threshold)
125
+
126
+ def _register_units(self, model: nn.Module, target_device: torch.device, enable_optimizer_cpu_offload: bool, cpu_offload_split_threshold: int = None):
127
+ self.memory_buffer = PinnedArenaPool.from_model(model)
128
+ units = self._find_units_recursive(model, cpu_offload_split_threshold)
129
+ self.units = [UnitWiseHookManager(u, target_device, enable_optimizer_cpu_offload, memory_buffer=self.memory_buffer) for u in units]
130
+
131
+ managed_param_ids = set().union(*[unit.managed_param_ids for unit in self.units])
132
+ orphan_params, orphan_buffers = self._find_orphan_params_and_buffers(model, managed_param_ids)
133
+ for orphan_module in set(orphan_params.keys()) | set(orphan_buffers.keys()):
134
+ params = orphan_params.get(orphan_module, [])
135
+ buffers = orphan_buffers.get(orphan_module, [])
136
+ self.units.append(UnitWiseHookManager(orphan_module, target_device, enable_optimizer_cpu_offload, params=params, buffers=buffers, memory_buffer=self.memory_buffer))
137
+
138
+ def _find_orphan_params_and_buffers(self, model: nn.Module, managed_param_ids: set):
139
+ orphan_params_by_module = {}
140
+ for _, mod in model.named_modules():
141
+ for param in mod.parameters(recurse=False):
142
+ if id(param) not in managed_param_ids:
143
+ orphan_params_by_module.setdefault(mod, []).append(param)
144
+ # Collect orphan buffers grouped by owner module
145
+ orphan_buffers_by_module = {}
146
+ for _, mod in model.named_modules():
147
+ for name, buf in mod.named_buffers(recurse=False):
148
+ orphan_buffers_by_module.setdefault(mod, []).append((mod, name, buf))
149
+
150
+ return orphan_params_by_module, orphan_buffers_by_module
151
+
152
+ def _find_units_recursive(self, module: nn.Module, cpu_offload_split_threshold: int = None) -> list:
153
+ if cpu_offload_split_threshold is None:
154
+ return [m for m in module.modules() if is_leaf_module(m) and has_parameters(m)]
155
+ if self._should_force_recurse(module, cpu_offload_split_threshold):
156
+ units = []
157
+ for child in module.children():
158
+ units.extend(self._find_units_recursive(child, cpu_offload_split_threshold))
159
+ return units
160
+ return [module]
161
+
162
+ def _should_force_recurse(self, module: nn.Module, cpu_offload_split_threshold: int = None) -> bool:
163
+ if is_leaf_module(module):
164
+ return False
165
+ if (
166
+ count_parameters(module) > cpu_offload_split_threshold
167
+ or ('forward' not in type(module).__dict__)
168
+ or (hasattr(module, 'encode') and hasattr(module, 'decode'))
169
+ ):
170
+ return True
171
+ return False
172
+
173
+ # run after backward() and before optimizer.step()
174
+ def after_backward(self):
175
+ for unit in self.units:
176
+ unit.after_backward()
177
+ torch.cuda.synchronize()
diffsynth/core/offload_training/memory_buffer.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ ALIGNMENT = 64
4
+
5
+ def _align_up(x: int, alignment: int = ALIGNMENT) -> int:
6
+ return (x + alignment - 1) // alignment * alignment
7
+
8
+ def _next_power_of_two(x: int) -> int:
9
+ """
10
+ Smallest power of two >= x.
11
+ For power-of-two x=2^k: (x-1) has bit_length=k, so 1<<k = x (unchanged).
12
+ For non-power-of-two: (x-1).bit_length() exceeds floor-log2(x), rounding up to next 2^n.
13
+ """
14
+ return 1 if x <= 1 else 1 << (x - 1).bit_length()
15
+
16
+ def _prev_power_of_two(x: int) -> int:
17
+ """Largest power-of-two <= x."""
18
+ return 1 if x <= 1 else 1 << (x.bit_length() - 1)
19
+
20
+ def _tensor_storage_size(tensor: torch.Tensor) -> int:
21
+ return _align_up(tensor.numel() * tensor.element_size())
22
+
23
+
24
+ class BaseBufferPool:
25
+ """Naive per-tensor pin_memory allocation. No pre-allocation, no memory saving."""
26
+
27
+ def allocate_like(self, tensor: torch.Tensor) -> torch.Tensor:
28
+ return tensor.pin_memory()
29
+
30
+ @classmethod
31
+ def from_model(cls, model: torch.nn.Module, **kwargs):
32
+ return cls()
33
+
34
+
35
+ class PinnedBuffer:
36
+ """Single pinned uint8 buffer with bump-pointer allocation. Lazy: actual memory allocated on first allocate_like."""
37
+
38
+ def __init__(self, size: int):
39
+ self._size = size
40
+ self._buf: torch.Tensor | None = None
41
+ self._offset = 0
42
+
43
+ def _ensure_allocated(self):
44
+ if self._buf is None:
45
+ self._buf = torch.empty(self._size, dtype=torch.uint8, device="cpu", pin_memory=True)
46
+
47
+ @property
48
+ def capacity(self) -> int:
49
+ return self._size
50
+
51
+ @property
52
+ def remaining(self) -> int:
53
+ return self._size if self._buf is None else self._buf.numel() - self._offset
54
+
55
+ @property
56
+ def used(self) -> int:
57
+ return self._offset
58
+
59
+ @classmethod
60
+ def from_tensor(cls, tensor: torch.Tensor, min_size: int = 1 * 1024**3):
61
+ size = max(_tensor_storage_size(tensor) + ALIGNMENT, min_size)
62
+ return cls(_next_power_of_two(size))
63
+
64
+ def allocate_like(self, tensor: torch.Tensor, *, copy: bool = True, non_blocking: bool = False) -> torch.Tensor | None:
65
+ """Try to allocate a view for tensor. Returns None if not enough space."""
66
+ num_bytes = tensor.numel() * tensor.element_size()
67
+ if num_bytes > self.remaining:
68
+ return None
69
+ self._ensure_allocated()
70
+ view = self._buf.narrow(0, self._offset, num_bytes).view(tensor.dtype).reshape(tuple(tensor.shape))
71
+ if copy:
72
+ view.copy_(tensor, non_blocking=bool(non_blocking and tensor.device.type == "cuda"))
73
+ self._offset = _align_up(self._offset + num_bytes)
74
+ return view
75
+
76
+
77
+ class PinnedArenaPool(BaseBufferPool):
78
+ """Pinned arena pool — pre-allocate pinned memory, avoid per-tensor cudaHostAlloc overhead.
79
+
80
+ Pool strategy:
81
+ 1. Sizing: from_model() scans all non-trainable params + buffers, sums their aligned sizes
82
+ as total_bytes. max_chunk_size is raised to fit the largest single tensor.
83
+ 2. Decomposition: total_bytes is split into power-of-two chunks (min_chunk_size ~ max_chunk_size).
84
+ Each chunk becomes one PinnedBuffer (lazy — actual pin_memory on first use).
85
+ 3. Allocation: allocate_like() sequentially probes each buffer for space (first-fit).
86
+ Each PinnedBuffer uses bump-pointer with ALIGNMENT padding between tensors.
87
+ 4. Growth: if all existing buffers are full, _grow() appends a new PinnedBuffer sized
88
+ to the requesting tensor (at least min_chunk_size, power-of-two rounded).
89
+ 5. Fallback: on any exception, falls back to per-tensor pin_memory().
90
+ """
91
+
92
+ def __init__(self, total_bytes: int, min_chunk_size: int = 1 * 1024**3, max_chunk_size: int = 4 * 1024**3):
93
+ self.min_chunk_size = _next_power_of_two(int(min_chunk_size))
94
+ self.max_chunk_size = _next_power_of_two(int(max_chunk_size))
95
+ self.min_chunk_size = min(self.min_chunk_size, self.max_chunk_size)
96
+ self._buffers = [PinnedBuffer(s) for s in self._decompose(total_bytes, self.min_chunk_size, self.max_chunk_size)]
97
+
98
+ @classmethod
99
+ def from_model(cls, model: torch.nn.Module, min_chunk_size: int = 1 * 1024**3, max_chunk_size: int = 4 * 1024**3):
100
+ """Size pool for all non-trainable params + buffers."""
101
+ tensors = [p for p in model.parameters() if not p.requires_grad] + list(model.buffers())
102
+ total = sum(_tensor_storage_size(t) for t in tensors)
103
+ max_tensor_size = max((_tensor_storage_size(t) for t in tensors), default=0)
104
+ max_chunk_size = _next_power_of_two(max_tensor_size) if max_tensor_size > max_chunk_size else max_chunk_size
105
+ return cls(total, min_chunk_size=min_chunk_size, max_chunk_size=max_chunk_size)
106
+
107
+ @staticmethod
108
+ def _decompose(total_bytes: int, min_chunk_size: int, max_chunk_size: int) -> list:
109
+ """Decompose total_bytes into power-of-two chunks capped by min/max."""
110
+ if total_bytes <= 0:
111
+ return []
112
+ chunks, remaining = [], total_bytes
113
+ while remaining > 0:
114
+ chunk = max(min(_prev_power_of_two(remaining), max_chunk_size), min_chunk_size)
115
+ chunks.append(chunk)
116
+ remaining -= chunk
117
+ chunks.sort(reverse=True)
118
+ return chunks
119
+
120
+ def _grow(self, tensor: torch.Tensor):
121
+ self._buffers.append(PinnedBuffer.from_tensor(tensor, min_size=self.min_chunk_size))
122
+
123
+ def allocate_like(self, tensor: torch.Tensor, *, copy: bool = True, require_contiguous: bool = True, non_blocking: bool = False) -> torch.Tensor:
124
+ """Allocate a pinned view. Falls back to per-tensor pin_memory on failure."""
125
+ src = tensor.detach()
126
+ if require_contiguous and not src.is_contiguous():
127
+ src = src.contiguous()
128
+ try:
129
+ for buf in self._buffers:
130
+ view = buf.allocate_like(src, copy=copy, non_blocking=non_blocking)
131
+ if view is not None:
132
+ return view
133
+ self._grow(src)
134
+ return self._buffers[-1].allocate_like(src, copy=copy, non_blocking=non_blocking)
135
+ except Exception:
136
+ return src.pin_memory()
diffsynth/core/offload_training/offloader.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from .memory_buffer import BaseBufferPool
3
+
4
+
5
+ class OffloaderMixin:
6
+ def onload(self):
7
+ pass
8
+
9
+ def offload(self):
10
+ pass
11
+
12
+ def offload_grad(self):
13
+ pass
14
+
15
+
16
+ class BaseParamOffloader(OffloaderMixin):
17
+ def __init__(self, param: torch.nn.Parameter, target_device: torch.device):
18
+ self.param = param
19
+ self.target_device = target_device
20
+
21
+
22
+ class StaticParamOffloader(BaseParamOffloader):
23
+ def __init__(self, param: torch.nn.Parameter, target_device: torch.device, memory_buffer: BaseBufferPool = None):
24
+ super().__init__(param, target_device)
25
+ cpu_data = param.data.cpu().detach().contiguous()
26
+ self.cpu_copy = memory_buffer.allocate_like(cpu_data) if memory_buffer is not None else cpu_data.pin_memory()
27
+ self._placeholder = torch.empty(0, device=target_device, dtype=param.dtype)
28
+ param.data = self._placeholder
29
+
30
+ def onload(self):
31
+ self.param.data = self.cpu_copy.to(self.target_device, non_blocking=True)
32
+
33
+ def offload(self):
34
+ self.param.data = self._placeholder
35
+
36
+
37
+ class TrainableParamOffloader(BaseParamOffloader):
38
+ def __init__(self, param: torch.nn.Parameter, target_device: torch.device):
39
+ super().__init__(param, target_device)
40
+ assert param.requires_grad, "TrainableParamOffloader can only be used with trainable parameters"
41
+
42
+ def onload(self):
43
+ self.param.data = self.param.data.to(self.target_device, non_blocking=True)
44
+
45
+ def offload(self):
46
+ self.param.data = self.param.data.to('cpu', non_blocking=True)
47
+
48
+ def offload_grad(self):
49
+ if self.param.grad is not None:
50
+ self.param.grad = self.param.grad.to('cpu', non_blocking=True)
51
+
52
+
53
+ class AlwaysOnGPUParamOffloader(BaseParamOffloader):
54
+ def __init__(self, param, target_device):
55
+ super().__init__(param, target_device)
56
+ self.param.data = self.param.data.to(self.target_device)
57
+
58
+
59
+ class BufferOffloader(OffloaderMixin):
60
+ def __init__(self, module: torch.nn.Module, buf_name: str, buf: torch.Tensor, target_device: torch.device, memory_buffer: BaseBufferPool = None):
61
+ self.module = module
62
+ self.buf_name = buf_name
63
+ self.target_device = target_device
64
+ cpu_data = buf.data.cpu().contiguous()
65
+ self.cpu_copy = memory_buffer.allocate_like(cpu_data) if memory_buffer is not None else cpu_data.pin_memory()
66
+
67
+ def onload(self):
68
+ self.module._buffers[self.buf_name] = self.cpu_copy.to(self.target_device, non_blocking=True)
69
+
70
+ def offload(self):
71
+ self.module._buffers[self.buf_name] = self.cpu_copy
diffsynth/core/quant/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .base import QuantBackend, BackendConfig, QUANT_BACKENDS, register_quant_backend, check_differentiable, check_backend_contract
2
+ from .config import QuantizeConfig, MixedQuantizeConfig, QuantMethodSpec, QUANT_METHODS, register_quant_method, describe_quant_method
3
+ from . import backends
diffsynth/core/quant/backends/__init__.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+
3
+ _LAZY_BACKENDS = {
4
+ "bitsandbytes": ".bitsandbytes",
5
+ "torchao": ".torchao",
6
+ "comfy_kitchen": ".comfy_kitchen",
7
+ }
8
+ _loaded = set()
9
+
10
+
11
+ def load_backend(name):
12
+ module = _LAZY_BACKENDS.get(name)
13
+ if module is not None and name not in _loaded:
14
+ importlib.import_module(module, __name__)
15
+ _loaded.add(name)
16
+
17
+
18
+ def load_all_backends():
19
+ for name in _LAZY_BACKENDS:
20
+ load_backend(name)
21
+
22
+
23
+ def load_backend_for_method(method):
24
+ from ..config import QUANT_METHODS
25
+ for name in _LAZY_BACKENDS:
26
+ load_backend(name)
27
+ if method in QUANT_METHODS:
28
+ return
diffsynth/core/quant/backends/bitsandbytes.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+
3
+ import torch
4
+ from ..base import QuantBackend, BackendConfig, register_quant_backend
5
+ from ..config import register_quant_method
6
+
7
+ try:
8
+ import bitsandbytes as bnb
9
+ BITSANDBYTES_AVAILABLE = True
10
+ except ImportError:
11
+ BITSANDBYTES_AVAILABLE = False
12
+
13
+
14
+ if BITSANDBYTES_AVAILABLE:
15
+ class BitsAndBytesLinear4bit(bnb.nn.Linear4bit):
16
+ def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs):
17
+ local_names = set()
18
+ for name, current in list(self._parameters.items()) + list(self._buffers.items()):
19
+ if current is None:
20
+ continue
21
+ local_names.add(name)
22
+ key = prefix + name
23
+ if key in state_dict:
24
+ value = state_dict[key]
25
+ if name in self._parameters:
26
+ self._parameters[name] = value if isinstance(value, torch.nn.Parameter) else torch.nn.Parameter(value, requires_grad=False)
27
+ else:
28
+ self._buffers[name] = value
29
+ elif strict:
30
+ missing_keys.append(key)
31
+ if strict:
32
+ for key in state_dict:
33
+ if key.startswith(prefix) and key[len(prefix):].split(".", 1)[0] not in local_names:
34
+ unexpected_keys.append(key)
35
+
36
+
37
+ @register_quant_backend("bitsandbytes")
38
+ class BitsAndBytesQuantBackend(QuantBackend):
39
+ """Adapter over `bitsandbytes.nn.Linear4bit` (NF4 / FP4, weight-only)."""
40
+
41
+ project_url = "https://github.com/bitsandbytes-foundation/bitsandbytes"
42
+
43
+ def validate_environment(self):
44
+ if not BITSANDBYTES_AVAILABLE:
45
+ raise ImportError(
46
+ "bitsandbytes is required for bitsandbytes quantization methods. "
47
+ 'Please install it via `pip install bitsandbytes` or `pip install "diffsynth[quant]"`.'
48
+ )
49
+
50
+ def capabilities(self):
51
+ return {
52
+ "is_serializable": True,
53
+ "is_differentiable": True,
54
+ "is_compileable": False,
55
+ "requires_calibration": False,
56
+ }
57
+
58
+ def quantized_linear_classes(self):
59
+ return (BitsAndBytesLinear4bit,)
60
+
61
+ def create_quantized_linear(self, linear, compute_device=None, model_device=None):
62
+ """The `meta` shell avoids allocating an fp weight; bnb quantizes while `Params4bit` moves to `compute_device`."""
63
+ cfg = self.config
64
+ compute_dtype = linear.weight.dtype
65
+ if compute_device is None:
66
+ compute_device = linear.weight.device
67
+
68
+ with torch.device("meta"):
69
+ quant_linear = BitsAndBytesLinear4bit(
70
+ linear.in_features,
71
+ linear.out_features,
72
+ bias=linear.bias is not None,
73
+ compute_dtype=compute_dtype,
74
+ compress_statistics=cfg.compress_statistics,
75
+ quant_type=cfg.quant_type,
76
+ quant_storage=cfg.quant_storage,
77
+ )
78
+ weight = bnb.nn.Params4bit(
79
+ linear.weight.data.contiguous(),
80
+ requires_grad=False,
81
+ compress_statistics=cfg.compress_statistics,
82
+ blocksize=cfg.blocksize,
83
+ quant_type=cfg.quant_type,
84
+ quant_storage=cfg.quant_storage,
85
+ )
86
+ quant_linear.weight = weight.to(compute_device)
87
+ if linear.bias is not None:
88
+ quant_linear.bias = torch.nn.Parameter(linear.bias.data.to(device=compute_device), requires_grad=False)
89
+ return quant_linear.to(device=model_device)
90
+
91
+ def create_quantized_linear_shell(self, linear, compute_dtype):
92
+ with torch.device("meta"):
93
+ shell = BitsAndBytesLinear4bit(
94
+ linear.in_features,
95
+ linear.out_features,
96
+ bias=linear.bias is not None,
97
+ compute_dtype=compute_dtype,
98
+ compress_statistics=self.config.compress_statistics,
99
+ quant_type=self.config.quant_type,
100
+ quant_storage=self.config.quant_storage,
101
+ )
102
+ return shell
103
+
104
+ def unflatten_state_dict(self, state_dict, metadata):
105
+ rebuilt = {}
106
+ quant_state = {}
107
+ for key, value in state_dict.items():
108
+ weight_key = key.rsplit(".weight.", 1)[0] + ".weight" if ".weight." in key else None
109
+ if weight_key is not None:
110
+ quant_state.setdefault(weight_key, {})[key] = value
111
+ else:
112
+ rebuilt[key] = value
113
+ for weight_key, stats in quant_state.items():
114
+ if weight_key not in rebuilt:
115
+ raise ValueError(f"Found quant state for `{weight_key}` but no packed weight.")
116
+ rebuilt[weight_key] = bnb.nn.Params4bit.from_prequantized(
117
+ data=rebuilt[weight_key], quantized_stats=stats, requires_grad=False, device=rebuilt[weight_key].device,
118
+ )
119
+ return rebuilt
120
+
121
+ def dequantize_to_linear(self, module, compute_dtype, compute_device=None, model_device=None):
122
+ if compute_device is not None:
123
+ module = module.to(device=compute_device)
124
+ weight = module.weight
125
+ fp_weight = bnb.functional.dequantize_4bit(weight.data, weight.quant_state).to(compute_dtype)
126
+ linear = torch.nn.Linear(module.in_features, module.out_features, bias=module.bias is not None, device="meta")
127
+ linear.weight = torch.nn.Parameter(fp_weight, requires_grad=False)
128
+ if module.bias is not None:
129
+ linear.bias = torch.nn.Parameter(module.bias.data.to(dtype=compute_dtype, device=fp_weight.device), requires_grad=False)
130
+ if model_device is not None:
131
+ linear = linear.to(device=model_device)
132
+ return linear
133
+
134
+
135
+ @dataclass
136
+ class BitsAndBytes4bitConfig(BackendConfig):
137
+ """Shared 4bit knobs; `quant_type` is pinned by the per-method subclass."""
138
+
139
+ compress_statistics: bool = True
140
+ blocksize: int = None # None -> bnb default (64)
141
+ quant_storage: torch.dtype = torch.uint8
142
+
143
+
144
+ @dataclass
145
+ class BitsAndBytesNF4Config(BitsAndBytes4bitConfig):
146
+ quant_type: str = field(init=False, default="nf4")
147
+
148
+
149
+ @dataclass
150
+ class BitsAndBytesFP4Config(BitsAndBytes4bitConfig):
151
+ quant_type: str = field(init=False, default="fp4")
152
+
153
+
154
+ register_quant_method("bitsandbytes_nf4", "bitsandbytes", BitsAndBytesNF4Config.from_kwargs, label="4bit, nf4, weight-only")
155
+ register_quant_method("bitsandbytes_fp4", "bitsandbytes", BitsAndBytesFP4Config.from_kwargs, label="4bit, fp4, weight-only")
diffsynth/core/quant/backends/comfy_kitchen.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import json
3
+ from dataclasses import dataclass, field
4
+
5
+ import torch
6
+ from ..base import QuantBackend, BackendConfig, register_quant_backend
7
+ from ..config import register_quant_method
8
+
9
+ try:
10
+ import comfy_kitchen as ck
11
+ from comfy_kitchen.tensor import QuantizedTensor, get_layout_class
12
+ COMFY_KITCHEN_AVAILABLE = True
13
+ except ImportError:
14
+ COMFY_KITCHEN_AVAILABLE = False
15
+
16
+
17
+ COMFY_QUANT_KEY = "comfy_quant"
18
+ SCALE_KEY = "weight_scale"
19
+
20
+
21
+ class ComfyKitchenLinear(torch.nn.Linear):
22
+ """`nn.Linear` whose weight is a comfy-kitchen `QuantizedTensor`."""
23
+
24
+ def __init__(self, in_features, out_features, bias, *, layout, compute_dtype):
25
+ with torch.device("meta"):
26
+ super().__init__(in_features, out_features, bias=bias, dtype=compute_dtype)
27
+ self.layout = layout
28
+ self.compute_dtype = compute_dtype
29
+ self.weight.requires_grad_(False)
30
+ if self.bias is not None:
31
+ self.bias.requires_grad_(False)
32
+
33
+ def forward(self, x):
34
+ if not x.is_contiguous():
35
+ x = x.contiguous()
36
+ return super().forward(x)
37
+
38
+ def __deepcopy__(self, memo):
39
+ clone = type(self)(
40
+ self.in_features, self.out_features, bias=self.bias is not None,
41
+ layout=self.layout, compute_dtype=self.compute_dtype,
42
+ )
43
+ weight = self.weight
44
+ params = getattr(weight, "_params", None)
45
+ if params is None:
46
+ cloned_weight = weight.detach().clone()
47
+ else:
48
+ cloned_weight = QuantizedTensor(weight._qdata.clone(), weight._layout_cls, copy.deepcopy(params))
49
+ clone.weight = torch.nn.Parameter(cloned_weight, requires_grad=False)
50
+ if self.bias is not None:
51
+ clone.bias = torch.nn.Parameter(self.bias.detach().clone(), requires_grad=False)
52
+ memo[id(self)] = clone
53
+ return clone
54
+
55
+
56
+ def _fp8_linear(x_2d, weight, bias, layout, input_scale):
57
+ """Quantize a 2D activation per tensor, then let comfy-kitchen's dispatch pick the fp8 matmul."""
58
+ q_x = QuantizedTensor.from_float(x_2d, layout, scale=input_scale)
59
+ return torch.nn.functional.linear(q_x, weight, bias)
60
+
61
+
62
+ class _FP8LinearFunction(torch.autograd.Function):
63
+
64
+ @staticmethod
65
+ def forward(ctx, x, weight, bias, layout, input_scale):
66
+ out = _fp8_linear(x.detach().reshape(-1, x.shape[-1]), weight, bias, layout, input_scale)
67
+ ctx.save_for_backward(weight)
68
+ ctx.x_shape = x.shape
69
+ return out.reshape(*x.shape[:-1], out.shape[-1])
70
+
71
+ @staticmethod
72
+ @torch.autograd.function.once_differentiable
73
+ def backward(ctx, grad_output):
74
+ weight, = ctx.saved_tensors
75
+ grad_2d = grad_output.reshape(-1, grad_output.shape[-1])
76
+ grad_x = torch.mm(grad_2d, weight.dequantize().to(grad_2d.dtype)).reshape(ctx.x_shape)
77
+ grad_bias = grad_2d.sum(dim=0) if ctx.needs_input_grad[2] else None
78
+ return grad_x, None, grad_bias, None, None
79
+
80
+
81
+ class ComfyKitchenFP8Linear(ComfyKitchenLinear):
82
+
83
+ def __init__(self, in_features, out_features, bias, *, layout, compute_dtype):
84
+ super().__init__(in_features, out_features, bias, layout=layout, compute_dtype=compute_dtype)
85
+ self.register_buffer("input_scale", None)
86
+
87
+ def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs):
88
+ scale = state_dict.pop(prefix + "input_scale", None)
89
+ super()._load_from_state_dict(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs)
90
+ if scale is not None:
91
+ self.input_scale = scale
92
+
93
+ def forward(self, x):
94
+ if not x.is_contiguous():
95
+ x = x.contiguous()
96
+ scale = self.input_scale
97
+ if scale is not None:
98
+ scale = scale.to(dtype=torch.float32, device=x.device)
99
+ if x.requires_grad:
100
+ return _FP8LinearFunction.apply(x, self.weight, self.bias, self.layout, scale)
101
+ out = _fp8_linear(x.reshape(-1, x.shape[-1]), self.weight, self.bias, self.layout, scale)
102
+ return out.reshape(*x.shape[:-1], out.shape[-1])
103
+
104
+ def __deepcopy__(self, memo):
105
+ clone = super().__deepcopy__(memo)
106
+ if self.input_scale is not None:
107
+ clone.input_scale = self.input_scale.detach().clone()
108
+ return clone
109
+
110
+
111
+ class ComfyQuantFormat:
112
+ """Everything specific to one weight format that ComfyUI writes, one hook per direction."""
113
+
114
+ marker_name = None # the `format` string ComfyUI writes into the marker
115
+ layout_cls = None # name of the comfy-kitchen layout class
116
+ linear_cls = ComfyKitchenLinear # the `nn.Linear` subclass that runs this format
117
+ sibling_keys = () # per-layer tensors to drop, so `load_state_dict` never sees them
118
+
119
+ def params_from_marker(self, marker, qdata, layer_name):
120
+ """`Params` fields beyond `scale`, `orig_dtype` and `orig_shape`, read off the marker."""
121
+ return {}
122
+
123
+ def from_float_kwargs(self, in_features, config):
124
+ """Keywords for `QuantizedTensor.from_float` when quantizing an fp weight online."""
125
+ return {}
126
+
127
+ def marker_from_params(self, params):
128
+ """Marker fields to write back, so a re-saved checkpoint reloads identically."""
129
+ return {}
130
+
131
+
132
+ class TensorWiseInt8Format(ComfyQuantFormat):
133
+
134
+ marker_name = "int8_tensorwise"
135
+ layout_cls = "TensorWiseINT8Layout"
136
+ DEFAULT_GROUPSIZE = 256
137
+
138
+ def params_from_marker(self, marker, qdata, layer_name):
139
+ nested = marker.get("params", {})
140
+ convrot = bool(marker.get("convrot", nested.get("convrot", False)))
141
+ groupsize = int(marker.get("convrot_groupsize", nested.get("convrot_groupsize", self.DEFAULT_GROUPSIZE)))
142
+ in_features = qdata.shape[1]
143
+ if convrot and in_features % groupsize:
144
+ raise ValueError(
145
+ f"The marker of `{layer_name}` asks for convrot with groupsize {groupsize}, "
146
+ f"which does not divide in_features={in_features}."
147
+ )
148
+ return {"is_weight": True, "convrot": convrot, "convrot_groupsize": groupsize}
149
+
150
+ def from_float_kwargs(self, in_features, config):
151
+ groupsize = config.convrot_groupsize
152
+ kwargs = {"is_weight": True, "per_channel": config.per_channel, "convrot": False, "convrot_groupsize": groupsize}
153
+ if not config.convrot:
154
+ return kwargs
155
+ if groupsize < 4 or groupsize & (groupsize - 1) or (groupsize.bit_length() - 1) % 2:
156
+ raise ValueError(f"convrot_groupsize must be a power of four >= 4, got {groupsize}.")
157
+ while groupsize >= 4 and in_features % groupsize:
158
+ groupsize //= 4
159
+ if groupsize >= 4:
160
+ kwargs.update(convrot=True, convrot_groupsize=groupsize)
161
+ return kwargs
162
+
163
+ def marker_from_params(self, params):
164
+ marker = {"convrot": bool(params.convrot)}
165
+ if params.convrot:
166
+ marker["convrot_groupsize"] = int(params.convrot_groupsize)
167
+ return marker
168
+
169
+
170
+ class Float8E4M3Format(ComfyQuantFormat):
171
+
172
+ marker_name = "float8_e4m3fn"
173
+ layout_cls = "TensorCoreFP8Layout"
174
+ linear_cls = ComfyKitchenFP8Linear
175
+
176
+
177
+ FORMATS = {fmt.marker_name: fmt for fmt in (TensorWiseInt8Format(), Float8E4M3Format())}
178
+
179
+
180
+ def _parse_marker(marker_tensor, layer_name):
181
+ try:
182
+ marker = json.loads(bytes(marker_tensor.to(torch.uint8).tolist()).decode())
183
+ except Exception as error:
184
+ raise ValueError(f"`{layer_name}.{COMFY_QUANT_KEY}` is not valid JSON: {error}")
185
+ marker_name = marker.get("format")
186
+ if marker_name is None:
187
+ raise ValueError(f"`{layer_name}.{COMFY_QUANT_KEY}` has no `format` field.")
188
+ if marker_name not in FORMATS:
189
+ raise ValueError(f"`{layer_name}` uses format `{marker_name}`, not in {sorted(FORMATS)}.")
190
+ return FORMATS[marker_name], marker
191
+
192
+
193
+ @register_quant_backend("comfy_kitchen")
194
+ class ComfyKitchenQuantBackend(QuantBackend):
195
+ """Adapter over comfy-kitchen's `QuantizedTensor`, the quantized-weight format ComfyUI writes."""
196
+
197
+ project_url = "https://github.com/Comfy-Org/comfy-kitchen"
198
+
199
+ def validate_environment(self):
200
+ if not COMFY_KITCHEN_AVAILABLE:
201
+ raise ImportError(
202
+ "comfy-kitchen is required for comfy_kitchen quantization methods. "
203
+ 'Please install it via `pip install comfy-kitchen` or `pip install "diffsynth[quant]"`.'
204
+ )
205
+ if torch.version.cuda is None or tuple(int(v) for v in torch.version.cuda.split(".")[:2]) < (13, 0):
206
+ ck.registry.disable("cuda")
207
+
208
+ def capabilities(self):
209
+ return {
210
+ "is_serializable": True,
211
+ "is_differentiable": True,
212
+ "is_compileable": False,
213
+ "requires_calibration": False,
214
+ }
215
+
216
+ def quantized_linear_classes(self):
217
+ return tuple(dict.fromkeys(fmt.linear_cls for fmt in FORMATS.values()))
218
+
219
+ def create_quantized_linear(self, linear, compute_device=None, model_device=None):
220
+ quant_format = self._active_format()
221
+ if compute_device is not None:
222
+ linear = linear.to(device=compute_device)
223
+ weight = QuantizedTensor.from_float(
224
+ linear.weight.data, quant_format.layout_cls,
225
+ **quant_format.from_float_kwargs(linear.in_features, self.config),
226
+ )
227
+ quant_linear = self._build_linear(linear, linear.weight.dtype)
228
+ quant_linear.weight = torch.nn.Parameter(weight, requires_grad=False)
229
+ if linear.bias is not None:
230
+ quant_linear.bias = torch.nn.Parameter(linear.bias.data, requires_grad=False)
231
+ return quant_linear if model_device is None else quant_linear.to(device=model_device)
232
+
233
+ def create_quantized_linear_shell(self, linear, compute_dtype):
234
+ return self._build_linear(linear, compute_dtype)
235
+
236
+ def unflatten_state_dict(self, state_dict, metadata):
237
+ rebuilt = dict(state_dict)
238
+ suffix = f".{COMFY_QUANT_KEY}"
239
+ for layer_name in [key[: -len(suffix)] for key in state_dict if key.endswith(suffix)]:
240
+ quant_format, marker = _parse_marker(rebuilt.pop(layer_name + suffix), layer_name)
241
+ qdata = rebuilt.get(f"{layer_name}.weight")
242
+ scale = rebuilt.pop(f"{layer_name}.{SCALE_KEY}", None)
243
+ for key, value in (("weight", qdata), (SCALE_KEY, scale)):
244
+ if value is None:
245
+ raise ValueError(f"`{layer_name}` has a {COMFY_QUANT_KEY} marker but no `{key}`.")
246
+ for key in quant_format.sibling_keys:
247
+ rebuilt.pop(f"{layer_name}.{key}", None)
248
+ params = get_layout_class(quant_format.layout_cls).Params(
249
+ scale=scale,
250
+ orig_dtype=self.config.orig_dtype,
251
+ orig_shape=tuple(qdata.shape),
252
+ **quant_format.params_from_marker(marker, qdata, layer_name),
253
+ )
254
+ rebuilt[f"{layer_name}.weight"] = QuantizedTensor(qdata, quant_format.layout_cls, params)
255
+ return rebuilt
256
+
257
+ def flatten_state_dict(self, state_dict):
258
+ flattened = {}
259
+ for key, value in state_dict.items():
260
+ params = getattr(value, "_params", None)
261
+ if not key.endswith(".weight") or params is None:
262
+ flattened[key] = value
263
+ continue
264
+ layer_name = key[: -len(".weight")]
265
+ quant_format = next((f for f in FORMATS.values() if f.layout_cls == value._layout_cls), None)
266
+ if quant_format is None:
267
+ raise ValueError(f"`{layer_name}` uses layout `{value._layout_cls}`, which no format claims.")
268
+ flattened.update(value.state_dict(prefix=key))
269
+ marker = {"format": quant_format.marker_name, **quant_format.marker_from_params(params)}
270
+ flattened[f"{layer_name}.{COMFY_QUANT_KEY}"] = torch.frombuffer(
271
+ bytearray(json.dumps(marker).encode()), dtype=torch.uint8
272
+ )
273
+ return flattened, {}
274
+
275
+ def dequantize_to_linear(self, module, compute_dtype, compute_device=None, model_device=None):
276
+ if compute_device is not None:
277
+ module = module.to(device=compute_device)
278
+ fp_weight = module.weight.dequantize().to(compute_dtype)
279
+ linear = torch.nn.Linear(module.in_features, module.out_features, bias=module.bias is not None, device="meta")
280
+ linear.weight = torch.nn.Parameter(fp_weight, requires_grad=False)
281
+ if module.bias is not None:
282
+ linear.bias = torch.nn.Parameter(module.bias.data.to(dtype=compute_dtype, device=fp_weight.device), requires_grad=False)
283
+ return linear if model_device is None else linear.to(device=model_device)
284
+
285
+ def _active_format(self):
286
+ marker_name = self.config.format
287
+ if marker_name not in FORMATS:
288
+ raise ValueError(f"Unsupported format `{marker_name}`, not in {sorted(FORMATS)}.")
289
+ return FORMATS[marker_name]
290
+
291
+ def _build_linear(self, linear, compute_dtype):
292
+ quant_format = self._active_format()
293
+ return quant_format.linear_cls(
294
+ linear.in_features,
295
+ linear.out_features,
296
+ bias=linear.bias is not None,
297
+ layout=quant_format.layout_cls,
298
+ compute_dtype=compute_dtype,
299
+ )
300
+
301
+
302
+ @dataclass
303
+ class ComfyKitchenInt8Config(BackendConfig):
304
+ format: str = field(init=False, default=TensorWiseInt8Format.marker_name) # pinned by the method
305
+ per_channel: bool = True
306
+ convrot: bool = True
307
+ convrot_groupsize: int = TensorWiseInt8Format.DEFAULT_GROUPSIZE
308
+ orig_dtype: torch.dtype = torch.bfloat16
309
+
310
+
311
+ @dataclass
312
+ class ComfyKitchenFp8Config(BackendConfig):
313
+ format: str = field(init=False, default=Float8E4M3Format.marker_name) # pinned by the method
314
+ orig_dtype: torch.dtype = torch.bfloat16
315
+
316
+
317
+ register_quant_method("comfy_kitchen_int8_w8a8", "comfy_kitchen", ComfyKitchenInt8Config.from_kwargs, label="W8A8, int8 weight + int8 dynamic activation (ComfyUI int8_tensorwise)")
318
+ register_quant_method("comfy_kitchen_fp8_w8a8", "comfy_kitchen", ComfyKitchenFp8Config.from_kwargs, label="W8A8, fp8 E4M3 weight + fp8 activation, static input_scale when present (ComfyUI float8_e4m3fn)")
diffsynth/core/quant/backends/torchao.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ import torch
4
+ from ..base import QuantBackend, register_quant_backend
5
+ from ..config import register_quant_method
6
+
7
+ try:
8
+ from torchao.quantization import (
9
+ quantize_, Int8WeightOnlyConfig, Int4WeightOnlyConfig, Float8WeightOnlyConfig,
10
+ Int8DynamicActivationInt8WeightConfig, Float8DynamicActivationFloat8WeightConfig,
11
+ Float8DynamicActivationInt4WeightConfig,
12
+ )
13
+ from torchao.prototype.mx_formats.inference_workflow import (
14
+ MXDynamicActivationMXWeightConfig, NVFP4DynamicActivationNVFP4WeightConfig, NVFP4WeightOnlyConfig,
15
+ )
16
+ from torchao.prototype.safetensors.safetensors_support import flatten_tensor_state_dict, unflatten_tensor_state_dict
17
+ from torchao.prototype.safetensors.safetensors_utils import is_metadata_torchao
18
+ TORCHAO_AVAILABLE = True
19
+ except ImportError:
20
+ TORCHAO_AVAILABLE = False
21
+
22
+
23
+ class TorchaoLinear(torch.nn.Linear):
24
+ """Marker class for torchao-quantized Linears."""
25
+
26
+
27
+ @register_quant_backend("torchao")
28
+ class TorchaoQuantBackend(QuantBackend):
29
+ """Adapter over torchao `quantize_` (weight-only configs); the quantization lives in the weight tensor subclass, not the module class."""
30
+
31
+ project_url = "https://github.com/pytorch/ao"
32
+
33
+ def validate_environment(self):
34
+ if not TORCHAO_AVAILABLE:
35
+ raise ImportError(
36
+ "torchao is required for torchao quantization methods. "
37
+ 'Please install it via `pip install torchao` or `pip install "diffsynth[quant]"`.'
38
+ )
39
+
40
+ _DIFFERENTIABLE_CONFIGS = frozenset({
41
+ "Int8WeightOnlyConfig",
42
+ "Float8WeightOnlyConfig",
43
+ "NVFP4WeightOnlyConfig",
44
+ })
45
+
46
+ def capabilities(self):
47
+ config_name = type(self.config).__name__
48
+ return {
49
+ "is_serializable": True,
50
+ "is_differentiable": config_name in self._DIFFERENTIABLE_CONFIGS,
51
+ "is_compileable": True,
52
+ "requires_calibration": False,
53
+ }
54
+
55
+ def quantized_linear_classes(self):
56
+ return (TorchaoLinear,)
57
+
58
+ def create_quantized_linear(self, linear, compute_device=None, model_device=None):
59
+ linear.requires_grad_(False)
60
+ if compute_device is not None:
61
+ linear = linear.to(device=compute_device)
62
+ quant_linear = TorchaoLinear(linear.in_features, linear.out_features, bias=linear.bias is not None, device="meta")
63
+ quant_linear.weight = linear.weight
64
+ quant_linear.bias = linear.bias
65
+ quantize_(quant_linear, self.config)
66
+ if model_device is not None:
67
+ quant_linear = quant_linear.to(device=model_device)
68
+ return quant_linear
69
+
70
+ def create_quantized_linear_shell(self, linear, compute_dtype):
71
+ return TorchaoLinear(linear.in_features, linear.out_features, bias=linear.bias is not None, device="meta")
72
+
73
+ def flatten_state_dict(self, state_dict):
74
+ return flatten_tensor_state_dict(state_dict)
75
+
76
+ def unflatten_state_dict(self, state_dict, metadata):
77
+ if not is_metadata_torchao(metadata):
78
+ raise ValueError("This checkpoint carries no torchao metadata.")
79
+ tensor_names = json.loads(metadata["tensor_names"])
80
+ root_names = [name for name in tensor_names if "." not in name]
81
+ if root_names:
82
+ metadata = {**metadata, "tensor_names": json.dumps([name for name in tensor_names if "." in name])}
83
+ rebuilt, _ = unflatten_tensor_state_dict(state_dict, metadata)
84
+ for name in root_names:
85
+ if name in state_dict:
86
+ rebuilt[name] = state_dict[name]
87
+ return rebuilt
88
+
89
+ @staticmethod
90
+ def _to_plain_weight(weight, compute_dtype):
91
+ try:
92
+ return weight.dequantize().to(compute_dtype)
93
+ except (NotImplementedError, AttributeError):
94
+ eye = torch.eye(weight.shape[1], dtype=compute_dtype, device=weight.device)
95
+ return torch.nn.functional.linear(eye, weight).t().contiguous()
96
+
97
+ def dequantize_to_linear(self, module, compute_dtype, compute_device=None, model_device=None):
98
+ if compute_device is not None:
99
+ module = module.to(device=compute_device)
100
+ fp_weight = self._to_plain_weight(module.weight, compute_dtype)
101
+ linear = torch.nn.Linear(module.in_features, module.out_features, bias=module.bias is not None, device="meta")
102
+ linear.weight = torch.nn.Parameter(fp_weight, requires_grad=False)
103
+ if module.bias is not None:
104
+ linear.bias = torch.nn.Parameter(module.bias.data.to(dtype=compute_dtype, device=fp_weight.device), requires_grad=False)
105
+ return linear if model_device is None else linear.to(device=model_device)
106
+
107
+
108
+ def _int8_weight_only(backend_config_kwargs):
109
+ backend_config_kwargs.setdefault("version", 2)
110
+ return Int8WeightOnlyConfig(**backend_config_kwargs)
111
+
112
+
113
+ def _int4_weight_only(backend_config_kwargs):
114
+ return Int4WeightOnlyConfig(**backend_config_kwargs)
115
+
116
+
117
+ def _fp8_weight_only(backend_config_kwargs):
118
+ return Float8WeightOnlyConfig(**backend_config_kwargs)
119
+
120
+
121
+ def _int8_dynamic_activation_int8_weight(backend_config_kwargs):
122
+ backend_config_kwargs.setdefault("version", 2)
123
+ return Int8DynamicActivationInt8WeightConfig(**backend_config_kwargs)
124
+
125
+
126
+ def _float8_dynamic_activation_float8_weight(backend_config_kwargs):
127
+ return Float8DynamicActivationFloat8WeightConfig(**backend_config_kwargs)
128
+
129
+
130
+ def _float8_dynamic_activation_int4_weight(backend_config_kwargs):
131
+ return Float8DynamicActivationInt4WeightConfig(**backend_config_kwargs)
132
+
133
+
134
+ def _mx_dynamic_activation_mx_weight(elem_dtype):
135
+ def factory(backend_config_kwargs):
136
+ dtype = getattr(torch, elem_dtype, None)
137
+ if dtype is None:
138
+ raise ImportError(f"This torch build has no `torch.{elem_dtype}`, required by the MX config.")
139
+ kwargs = {"block_size": 32, "activation_dtype": dtype, "weight_dtype": dtype}
140
+ kwargs.update(backend_config_kwargs)
141
+ return MXDynamicActivationMXWeightConfig(**kwargs)
142
+ return factory
143
+
144
+
145
+ def _nvfp4_dynamic_activation_nvfp4_weight(backend_config_kwargs):
146
+ return NVFP4DynamicActivationNVFP4WeightConfig(**backend_config_kwargs)
147
+
148
+
149
+ def _nvfp4_weight_only(backend_config_kwargs):
150
+ return NVFP4WeightOnlyConfig(**backend_config_kwargs)
151
+
152
+
153
+ register_quant_method("torchao_int8_w8a16", "torchao", _int8_weight_only, label="8bit, int8, weight-only")
154
+ register_quant_method("torchao_int4_w4a16", "torchao", _int4_weight_only, label="W4A16, int4 weight-only")
155
+ register_quant_method("torchao_fp8_w8a16", "torchao", _fp8_weight_only, label="8bit, fp8, weight-only")
156
+ register_quant_method("torchao_int8_w8a8", "torchao", _int8_dynamic_activation_int8_weight, label="W8A8, int8 weight + int8 dynamic activation")
157
+ register_quant_method("torchao_fp8_w8a8", "torchao", _float8_dynamic_activation_float8_weight, label="W8A8, fp8 weight + fp8 dynamic activation")
158
+ register_quant_method("torchao_int4_w4a8", "torchao", _float8_dynamic_activation_int4_weight, label="W4A8, int4 weight + fp8 dynamic activation")
159
+ register_quant_method("torchao_mxfp8_w8a8", "torchao", _mx_dynamic_activation_mx_weight("float8_e4m3fn"), label="W8A8, MXFP8 microscaling weight + activation")
160
+ register_quant_method("torchao_mxfp4_w4a4", "torchao", _mx_dynamic_activation_mx_weight("float4_e2m1fn_x2"), label="W4A4, MXFP4 microscaling weight + activation")
161
+ register_quant_method("torchao_nvfp4_w4a4", "torchao", _nvfp4_dynamic_activation_nvfp4_weight, label="W4A4, NVFP4 weight + activation")
162
+ register_quant_method("torchao_nvfp4_w4a16", "torchao", _nvfp4_weight_only, label="W4A16, NVFP4 weight-only")
diffsynth/core/quant/base.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC
2
+ from dataclasses import dataclass, fields
3
+ import torch
4
+
5
+
6
+ QUANT_BACKENDS = {}
7
+
8
+
9
+ @dataclass
10
+ class BackendConfig:
11
+ """Base for a backend's typed config. User-tunable knobs are ordinary init fields;
12
+ method-pinned values use `field(init=False, ...)` so they cannot be overridden."""
13
+
14
+ @classmethod
15
+ def from_kwargs(cls, kwargs):
16
+ accepted = {f.name for f in fields(cls) if f.init}
17
+ unknown = set(kwargs) - accepted
18
+ if unknown:
19
+ raise ValueError(
20
+ f"`backend_config_kwargs` {sorted(unknown)} are not accepted by `{cls.__name__}`. Accepted: {sorted(accepted) or '(none)'}."
21
+ )
22
+ return cls(**kwargs)
23
+
24
+
25
+ class QuantBackend(ABC):
26
+ """
27
+ Adapter between the framework and a quantization library (bnb / torchao / custom).
28
+ Subclasses are registered in `QUANT_BACKENDS` ({name -> class}) and instantiated by
29
+ `QuantizeConfig` with the method's backend config. A backend operates on single
30
+ Linears; model-level traversal and replacement is done by the `QuantizeConfig` methods.
31
+
32
+ The quantized Linear produced by a backend must satisfy:
33
+ (a) It is an `nn.Linear` drop-in: `forward(x)` internally performs dequant + matmul.
34
+ (b) `.to(...)` moves devices but never re-types the packed weight / quant state:
35
+ a dtype cast (`.to(dtype)` / `.half()` / `.float()`) must leave their storage
36
+ format and values intact.
37
+ (c) `state_dict()` / `load_state_dict(assign=True)` round-trips (optionally via
38
+ `flatten_state_dict` / `unflatten_state_dict`).
39
+ (d) (Training branch only) `forward` is differentiable w.r.t. its input, so
40
+ gradients can pass through frozen quantized layers to reach LoRA branches.
41
+ Declared statically by `capabilities()["is_differentiable"]` and verifiable
42
+ at runtime by calling `check_differentiable` on a quantized Linear the
43
+ backend produced.
44
+ """
45
+
46
+ name: str = ""
47
+ project_url: str = ""
48
+
49
+ def __init__(self, config=None):
50
+ self.config = config
51
+
52
+ def capabilities(self) -> dict:
53
+ return {
54
+ "is_serializable": False,
55
+ "is_differentiable": True,
56
+ "is_compileable": False,
57
+ "requires_calibration": False,
58
+ }
59
+
60
+ def validate_environment(self):
61
+ return
62
+
63
+ def announce_environment(self):
64
+ """Point at the library that owns this backend's hardware support, instead of restating it."""
65
+ if self.project_url:
66
+ print(f"Quantization backend `{self.name}`: for hardware compatibility issues see {self.project_url}")
67
+
68
+ def create_quantized_linear(self, linear: torch.nn.Linear, compute_device=None, model_device=None) -> torch.nn.Module:
69
+ raise NotImplementedError(
70
+ f"Backend `{self.name}` cannot quantize an fp model online. Use a method whose "
71
+ "backend supports it, or load an already quantized checkpoint."
72
+ )
73
+
74
+ def create_quantized_linear_shell(self, linear: torch.nn.Linear, compute_dtype: torch.dtype) -> torch.nn.Module:
75
+ raise NotImplementedError(
76
+ f"Backend `{self.name}` cannot load pre-quantized checkpoints. Use "
77
+ "`load_prequantized=False` to quantize an fp model online instead."
78
+ )
79
+
80
+ def dequantize_to_linear(self, module: torch.nn.Module, compute_dtype: torch.dtype, compute_device=None, model_device=None) -> torch.nn.Linear:
81
+ raise NotImplementedError(
82
+ f"Backend `{self.name}` cannot dequantize back to `nn.Linear`, so "
83
+ '`mode="dequant_once"` is unavailable.'
84
+ )
85
+
86
+ def quantized_linear_classes(self) -> tuple:
87
+ """The Linear classes this backend produces. MUST be subclass of `torch.nn.Linear`."""
88
+ raise NotImplementedError(
89
+ f"Backend `{self.name}` must declare the Linear classes it produces."
90
+ )
91
+
92
+ def is_quantized_linear(self, module) -> bool:
93
+ return isinstance(module, self.quantized_linear_classes())
94
+
95
+ def flatten_state_dict(self, state_dict: dict):
96
+ self._require_serializable()
97
+ return state_dict, {}
98
+
99
+ def unflatten_state_dict(self, state_dict: dict, metadata: dict):
100
+ self._require_serializable()
101
+ return state_dict
102
+
103
+ def _require_serializable(self):
104
+ if not self.capabilities().get("is_serializable", False):
105
+ raise NotImplementedError(
106
+ f"Backend `{self.name}` declares `is_serializable=False`, so its quantized state "
107
+ "dict cannot be flattened or rebuilt. Override these methods if it actually can."
108
+ )
109
+
110
+ def register_quant_backend(name):
111
+ def decorator(cls):
112
+ cls.name = name
113
+ QUANT_BACKENDS[name] = cls
114
+ return cls
115
+ return decorator
116
+
117
+
118
+ def check_differentiable(module: torch.nn.Module, example_input: torch.Tensor = None, verbose: bool = True) -> bool:
119
+ """
120
+ Check whether gradients pass through `module` w.r.t. its input: run a real
121
+ backward pass from the output (`torch.autograd.grad`) and verify a finite
122
+ gradient arrives at the input. This is what LoRA training requires from
123
+ frozen (e.g. quantized) layers. The module is cast to bfloat16 in place and
124
+ probed with a bfloat16 input; if `example_input` is None, a random one is
125
+ built for modules exposing `in_features`.
126
+
127
+ Example (probing a torchao-quantized Linear):
128
+
129
+ import torch
130
+ from diffsynth.core.quant import check_differentiable
131
+ from torchao.quantization import quantize_, Int8WeightOnlyConfig
132
+
133
+ linear = torch.nn.Linear(1024, 1024, dtype=torch.bfloat16, device="cuda")
134
+ quantize_(linear, Int8WeightOnlyConfig(version=2))
135
+ check_differentiable(linear)
136
+ """
137
+ def report(result, detail):
138
+ if verbose:
139
+ print(f"check_differentiable ({type(module).__name__}): {'OK' if result else 'FAIL'} -- {detail}")
140
+ return result
141
+
142
+ try:
143
+ module = module.to(torch.bfloat16)
144
+ if example_input is None:
145
+ if not hasattr(module, "in_features"):
146
+ raise ValueError("`example_input` is required for modules without `in_features`.")
147
+ device = next((t.device for t in list(module.parameters()) + list(module.buffers())), torch.device("cpu"))
148
+ example_input = torch.randn(4, module.in_features, device=device)
149
+ x = example_input.detach().to(torch.bfloat16).requires_grad_(True)
150
+ y = module(x)
151
+ if not y.requires_grad:
152
+ return report(False, "the output does not require grad, so no autograd graph was recorded")
153
+ input_grad = torch.autograd.grad(y, x, grad_outputs=torch.randn_like(y), allow_unused=True)[0]
154
+ except Exception as error:
155
+ return report(False, f"{type(error).__name__}: {error}")
156
+ if input_grad is None:
157
+ return report(False, "backward finished but no gradient reached the input")
158
+ if not torch.isfinite(input_grad.float()).all():
159
+ return report(False, "the input gradient contains non-finite values")
160
+ return report(True, "gradients pass through the module to its input")
161
+
162
+
163
+ def check_backend_contract(backend, in_features: int = 512, out_features: int = 512,
164
+ compute_dtype: torch.dtype = torch.bfloat16,
165
+ compute_device: str = "cuda", verbose: bool = True) -> bool:
166
+ """
167
+ Verify a backend satisfies the quantized-Linear contract: it declares its classes,
168
+ both factory methods return instances of them, and every declared class subclasses
169
+ `torch.nn.Linear` so LoRA target detection and VRAM management can see it. The
170
+ checkpoint key patterns are checked against the keys the backend actually writes,
171
+ since a pattern list that misses a scale makes disk offload load corrupt layers
172
+ without raising.
173
+
174
+ Example:
175
+
176
+ from diffsynth.core.quant import QUANT_BACKENDS, QUANT_METHODS, check_backend_contract
177
+ spec = QUANT_METHODS["bitsandbytes_nf4"]
178
+ check_backend_contract(QUANT_BACKENDS[spec.backend](spec.config_factory({})))
179
+ """
180
+ failures = []
181
+ backend.validate_environment()
182
+
183
+ def check(condition, detail):
184
+ if not condition:
185
+ failures.append(detail)
186
+ if verbose:
187
+ print(f" [{'PASS' if condition else 'FAIL'}] {detail}")
188
+ return condition
189
+
190
+ if verbose:
191
+ print(f"check_backend_contract ({backend.name}):")
192
+ try:
193
+ classes = backend.quantized_linear_classes()
194
+ except NotImplementedError as error:
195
+ if verbose:
196
+ print(f" [FAIL] quantized_linear_classes() is not implemented: {error}")
197
+ return False
198
+ if not check(len(classes) > 0, f"quantized_linear_classes() is non-empty: {[cls.__name__ for cls in classes]}"):
199
+ return False
200
+ for cls in classes:
201
+ check(issubclass(cls, torch.nn.Linear), f"{cls.__name__} subclasses torch.nn.Linear")
202
+
203
+ plain = torch.nn.Linear(in_features, out_features, bias=True, dtype=compute_dtype, device=compute_device)
204
+ plain.requires_grad_(False)
205
+ check(not backend.is_quantized_linear(plain), "a plain nn.Linear is not reported as quantized")
206
+
207
+ try:
208
+ shell = backend.create_quantized_linear_shell(plain, compute_dtype)
209
+ except NotImplementedError:
210
+ shell = None
211
+ if verbose:
212
+ print(" [SKIP] create_quantized_linear_shell() is unsupported by this backend")
213
+ if shell is not None:
214
+ check(isinstance(shell, classes), f"create_quantized_linear_shell() returns a declared class, got {type(shell).__name__}")
215
+ check(backend.is_quantized_linear(shell), "the shell is recognized before load_state_dict (disk offload routing)")
216
+
217
+ try:
218
+ quantized = backend.create_quantized_linear(plain, compute_device=compute_device)
219
+ except NotImplementedError:
220
+ quantized = None
221
+ if verbose:
222
+ print(" [SKIP] create_quantized_linear() is unsupported by this backend")
223
+ if quantized is not None:
224
+ check(isinstance(quantized, classes), f"create_quantized_linear() returns a declared class, got {type(quantized).__name__}")
225
+
226
+ saved = quantized if quantized is not None else shell
227
+ if saved is None:
228
+ if verbose:
229
+ print(" [SKIP] neither factory method is supported, so the stored keys cannot be checked")
230
+ else:
231
+ state_dict = {f"proj.{key}": value for key, value in saved.state_dict().items()}
232
+ if backend.capabilities().get("is_serializable", False):
233
+ try:
234
+ state_dict = backend.flatten_state_dict(state_dict)[0]
235
+ except Exception as error:
236
+ if verbose:
237
+ print(f" [SKIP] flatten_state_dict() failed, falling back to the raw state dict keys: {type(error).__name__}: {error}")
238
+ uncovered = sorted(key for key in state_dict if not key.startswith("proj."))
239
+ check(len(uncovered) == 0, f"every stored key lives under the layer name; uncovered: {uncovered}")
240
+
241
+ if verbose:
242
+ print(f" => {'OK' if not failures else str(len(failures)) + ' FAILED'}")
243
+ return not failures
diffsynth/core/quant/config.py ADDED
@@ -0,0 +1,570 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field, fields
2
+ from typing import Any, Callable, Optional
3
+ import torch
4
+ from .base import QUANT_BACKENDS
5
+
6
+
7
+ QUANT_METHODS = {}
8
+
9
+
10
+ @dataclass
11
+ class QuantMethodSpec:
12
+ backend: str
13
+ config_factory: Callable[[dict], Any]
14
+ label: str = ""
15
+
16
+
17
+ def register_quant_method(name, backend, config_factory, label=""):
18
+ QUANT_METHODS[name] = QuantMethodSpec(backend=backend, config_factory=config_factory, label=label)
19
+
20
+
21
+ def describe_quant_method(name):
22
+ """Print a method's backend, label, and the accepted `backend_config_kwargs` with defaults."""
23
+ from . import backends
24
+ backends.load_all_backends()
25
+ if name not in QUANT_METHODS:
26
+ raise ValueError(f"Unknown quantization method: {name}. Available methods:\n{_available_methods()}")
27
+ spec = QUANT_METHODS[name]
28
+ lines = [f"method: {name}", f"backend: {spec.backend}", f"detail: {spec.label}"]
29
+ try:
30
+ config = spec.config_factory({})
31
+ except Exception as error:
32
+ lines.append(f"backend config: could not be built with default kwargs: {error}")
33
+ print("\n".join(lines))
34
+ return
35
+ cls = type(config)
36
+ lines.append(f"backend config: {cls.__module__}.{cls.__qualname__}")
37
+ all_fields = fields(config)
38
+ user_fields = [f for f in all_fields if f.init]
39
+ pinned_fields = [f for f in all_fields if not f.init]
40
+ if user_fields:
41
+ lines.append("backend_config_kwargs (user-tunable):")
42
+ width = max(len(f.name) for f in user_fields)
43
+ lines += [f" {f.name:<{width}} = {getattr(config, f.name)!r}" for f in user_fields]
44
+ if pinned_fields:
45
+ lines.append("pinned by method (not overridable):")
46
+ lines += [f" {f.name} = {getattr(config, f.name)!r}" for f in pinned_fields]
47
+ print("\n".join(lines))
48
+
49
+
50
+ def _available_methods():
51
+ from . import backends
52
+ backends.load_all_backends()
53
+ methods = sorted(QUANT_METHODS.items())
54
+ if len(methods) == 0:
55
+ return " (no method registered)"
56
+ name_width = max(len(name) for name, _ in methods)
57
+ backend_width = max(len(spec.backend) for _, spec in methods)
58
+ lines = [f"method: {name:<{name_width}} backend: {spec.backend:<{backend_width}} detail: {spec.label}"
59
+ for name, spec in methods]
60
+ width = max(len(line) for line in lines)
61
+ body = "\n".join(f"│ {line:<{width}} │" for line in lines)
62
+ return f"┌{'─' * (width + 2)}┐\n{body}\n└{'─' * (width + 2)}┘"
63
+
64
+
65
+ def _format_error_report(reports):
66
+ if not reports:
67
+ return "Quantization error report: no targeted nn.Linear layers found."
68
+ methods = sorted({report["method"] for report in reports})
69
+ lines = [f"Quantization error report ({' + '.join(methods)}, {len(reports)} layers, sorted by relative_error, worst first):"]
70
+ mixed = len(methods) > 1
71
+ name_width = max(len(report["name"]) for report in reports)
72
+ header = f" {'relative_error':>14} {'max_abs':>12} {'shape':<16} {'name':<{name_width}}"
73
+ if mixed:
74
+ header += " method"
75
+ lines.append(header)
76
+ for report in reports:
77
+ line = (
78
+ f" {report['relative_error'] * 100:>13.2f}% {report['max_abs']:>12.3e}"
79
+ f" {str(report['shape']):<16} {report['name']:<{name_width}}"
80
+ )
81
+ if mixed:
82
+ line += f" {report['method']}"
83
+ lines.append(line)
84
+ return "\n".join(lines)
85
+
86
+
87
+ @dataclass
88
+ class QuantizeConfig:
89
+ """
90
+ Quantization config and entry points for any `nn.Module`.
91
+
92
+ Online (dynamic) quantization -- quantize an fp model whose weights are loaded:
93
+
94
+ cfg = QuantizeConfig(method="bitsandbytes_nf4")
95
+ model.load_state_dict(fp_state_dict)
96
+ cfg.quantize_model(model, compute_device="cuda")
97
+
98
+ Loading a pre-quantized checkpoint -- swap in quantized shells before loading:
99
+
100
+ cfg = QuantizeConfig(method="bitsandbytes_nf4", load_prequantized=True)
101
+ cfg.prepare_for_prequantized_load(model, compute_dtype=torch.bfloat16)
102
+ state_dict = cfg.unflatten_state_dict(state_dict, metadata)
103
+ model.load_state_dict(state_dict, assign=True)
104
+
105
+ Dequantize-once -- after either flow above, back to plain fp Linears that carry
106
+ the quantization error:
107
+
108
+ cfg.dequantize_model(model, compute_dtype=torch.bfloat16)
109
+
110
+ Fields:
111
+ method: name from QUANT_METHODS, determines backend + scheme + backend config.
112
+ mode: "dynamic" keeps the backend-native quantized Linear, which dequantizes
113
+ its weight on every forward; "dequant_once" dequantizes to a plain fp
114
+ `nn.Linear` right after the weights are quantized or loaded.
115
+ target_modules / exclude_modules: list of module names; a layer is matched if
116
+ its full dotted name equals an entry, or ends with "." + entry
117
+ (e.g. "img_mod.1" matches "transformer_blocks.0.img_mod.1").
118
+ backend_config_kwargs: passed through to the method's backend config factory
119
+ (e.g. nf4 blocksize); `describe_quant_method(name)` prints what a method
120
+ accepts.
121
+ load_prequantized: the checkpoint already holds quantized weights; load them
122
+ directly instead of quantizing fp weights online.
123
+ """
124
+ method: str = None
125
+ mode: str = "dynamic"
126
+ target_modules: Optional[list] = None
127
+ exclude_modules: Optional[list] = None
128
+ backend_config_kwargs: dict = field(default_factory=dict)
129
+ load_prequantized: bool = False
130
+
131
+ def __post_init__(self):
132
+ if self.method is None:
133
+ raise ValueError(f"`QuantizeConfig.method` is required. Available methods:\n{_available_methods()}")
134
+ if self.mode not in ("dynamic", "dequant_once"):
135
+ raise ValueError(f"`QuantizeConfig.mode` should be `dynamic` or `dequant_once`, but got `{self.mode}`.")
136
+ for field_name in ("target_modules", "exclude_modules"):
137
+ value = getattr(self, field_name)
138
+ if value is not None and not isinstance(value, list):
139
+ raise ValueError(f"`QuantizeConfig.{field_name}` should be a list of module names, but got `{type(value).__name__}`.")
140
+ self.backend = self._build_backend()
141
+
142
+ def _build_backend(self):
143
+ from . import backends
144
+ backends.load_backend_for_method(self.method)
145
+ if self.method not in QUANT_METHODS:
146
+ raise ValueError(f"Unknown quantization method: {self.method}. Available methods:\n{_available_methods()}")
147
+ spec = QUANT_METHODS[self.method]
148
+ if spec.backend not in QUANT_BACKENDS:
149
+ raise ValueError(f"Quantization backend `{spec.backend}` (required by method `{self.method}`) is not registered.")
150
+ backend = QUANT_BACKENDS[spec.backend]()
151
+ backend.validate_environment()
152
+ backend.announce_environment()
153
+ backend.config = spec.config_factory(dict(self.backend_config_kwargs))
154
+ return backend
155
+
156
+ def quantize_model(self, model: torch.nn.Module, compute_device=None, model_device=None):
157
+ """
158
+ Quantize the targeted `nn.Linear` layers of an fp model in place, keeping each
159
+ layer's existing dtype. Call AFTER `load_state_dict`. Does nothing when
160
+ `load_prequantized` is set, since such a checkpoint is already quantized.
161
+
162
+ Parameters:
163
+ model: the fp model whose targeted layers are replaced by quantized ones.
164
+ compute_device: where quantization runs; `None` quantizes each layer in place.
165
+ model_device: where each quantized layer is stored afterwards; `None` leaves it
166
+ on `compute_device`. Combining an fp model on the CPU with
167
+ `compute_device="cuda", model_device="cpu"` streams the work layer by
168
+ layer, so the accelerator only ever holds one layer at a time.
169
+ """
170
+ if self.load_prequantized:
171
+ return model
172
+
173
+ def quantize(linear):
174
+ return self.backend.create_quantized_linear(linear, compute_device=compute_device, model_device=model_device)
175
+
176
+ replaced = self._replace_target_linears(model, quantize)
177
+ print(f"{len(replaced)} nn.Linear layers quantized (method: {self.method}).")
178
+ return model
179
+
180
+ def dequantize_model(self, model: torch.nn.Module, compute_dtype: torch.dtype = torch.bfloat16, compute_device=None, model_device=None):
181
+ """
182
+ Replace every quantized Linear in the model by a plain fp `nn.Linear`.
183
+ Does nothing unless `mode` is `"dequant_once"`.
184
+
185
+ Parameters:
186
+ model: the quantized model to restore in place.
187
+ compute_dtype: dtype of the restored fp weights.
188
+ compute_device: where dequantization runs; `None` restores each layer in place.
189
+ model_device: where each restored layer is stored afterwards; `None` leaves it
190
+ on `compute_device`.
191
+ """
192
+ if self.mode != "dequant_once":
193
+ return model
194
+ self._dequantize_linears(model, compute_dtype, compute_device, model_device)
195
+ return model
196
+
197
+ def is_quantized_linear(self, module) -> bool:
198
+ """Whether `module` is one of this config's backend-native quantized Linears."""
199
+ return self.backend.is_quantized_linear(module)
200
+
201
+ def build_quantized_shell(self, module, compute_dtype: torch.dtype, **kwargs):
202
+ """
203
+ Build an empty quantized Linear matching `module`, used to release a layer's
204
+ weights while keeping it routable, and to stage a transient copy on the
205
+ computation device.
206
+
207
+ Parameters:
208
+ module: the quantized layer whose shape and bias presence are mirrored.
209
+ compute_dtype: dtype the shell dequantizes to at forward time.
210
+ """
211
+ return self.backend.create_quantized_linear_shell(module, compute_dtype)
212
+
213
+ def prepare_for_prequantized_load(self, model: torch.nn.Module, compute_dtype: torch.dtype = torch.bfloat16):
214
+ """
215
+ Replace the targeted `nn.Linear` layers with empty quantized Linears matching a
216
+ pre-quantized checkpoint. Call BEFORE `load_state_dict(assign=True)`.
217
+
218
+ Parameters:
219
+ model: the freshly constructed model whose targeted layers become shells.
220
+ compute_dtype: dtype the quantized layers dequantize to at forward time.
221
+ """
222
+ def build_shell(linear):
223
+ return self.backend.create_quantized_linear_shell(linear, compute_dtype)
224
+
225
+ replaced = self._replace_target_linears(model, build_shell)
226
+ print(f"{len(replaced)} nn.Linear layers replaced for loading the pre-quantized checkpoint (method: {self.method}).")
227
+ return model
228
+
229
+ def unflatten_state_dict(self, state_dict: dict, metadata: dict):
230
+ """
231
+ Rebuild composite quantized tensors from a flat (safetensors) state dict, so the
232
+ result can be given to `load_state_dict(assign=True)`.
233
+
234
+ Parameters:
235
+ state_dict: the flat tensors read from the checkpoint.
236
+ metadata: the string-only metadata stored alongside them.
237
+ """
238
+ return self.backend.unflatten_state_dict(state_dict, metadata)
239
+
240
+ def flatten_state_dict(self, state_dict: dict):
241
+ """
242
+ Flatten a quantized model's state dict into plain tensors and string-only metadata.
243
+ Returns `(tensors, metadata)`, ready for
244
+ `safetensors.torch.save_file(tensors, path, metadata=metadata)`.
245
+
246
+ Parameters:
247
+ state_dict: the quantized model's state dict, holding composite tensors.
248
+ """
249
+ if not self.backend.capabilities().get("is_serializable", False):
250
+ raise NotImplementedError(
251
+ f"Backend `{self.backend.name}` (method `{self.method}`) does not declare "
252
+ 'serialization support (`capabilities()["is_serializable"]`), so its quantized '
253
+ "state dict cannot be flattened for saving."
254
+ )
255
+ tensors, metadata = self.backend.flatten_state_dict(state_dict)
256
+ tensors = {key: value.contiguous() for key, value in tensors.items()}
257
+ metadata = {"format": "pt", **{key: value if isinstance(value, str) else str(value) for key, value in metadata.items()}}
258
+ return tensors, metadata
259
+
260
+ def measure_quantization_error(self, model: torch.nn.Module, compute_device="cuda", verbose=True) -> list:
261
+ """
262
+ Measure the weight error this config's quantization introduces.
263
+
264
+ Parameters:
265
+ model: the fp model to analyze.
266
+ compute_device: where the quantize-dequantize round trip runs.
267
+ verbose: print the per-layer report, sorted by `relative_error`, worst first.
268
+
269
+ Returns:
270
+ A list of per-layer dicts: `name`, `method`, `shape`, `relative_error` and `max_abs` (worst-element absolute error).
271
+ """
272
+ reports = [
273
+ self._measure_layer_error(full_name, module, compute_device)
274
+ for full_name, module in model.named_modules()
275
+ if full_name != "" and self._should_quantize(full_name, module)
276
+ ]
277
+ reports.sort(key=lambda report: report["relative_error"], reverse=True)
278
+ if verbose:
279
+ print(_format_error_report(reports))
280
+ return reports
281
+
282
+ def _measure_layer_error(self, full_name, module, compute_device):
283
+ fp_weight = module.weight.detach().to(device=compute_device, dtype=torch.float32)
284
+ linear = torch.nn.Linear(
285
+ module.in_features, module.out_features,
286
+ bias=module.bias is not None, dtype=module.weight.dtype, device=compute_device,
287
+ )
288
+ linear.weight.data.copy_(module.weight.detach())
289
+ if module.bias is not None:
290
+ linear.bias.data.copy_(module.bias.detach().to(device=compute_device))
291
+ quantized = self.backend.create_quantized_linear(linear, compute_device=compute_device, model_device=compute_device)
292
+ restored = self.backend.dequantize_to_linear(quantized, compute_dtype=torch.float32, compute_device=compute_device, model_device=compute_device)
293
+ quant_weight = restored.weight.detach()
294
+ error = quant_weight - fp_weight
295
+ report = {
296
+ "name": full_name,
297
+ "method": self.method,
298
+ "shape": tuple(fp_weight.shape),
299
+ "relative_error": (torch.linalg.vector_norm(error) / torch.linalg.vector_norm(fp_weight)).item(),
300
+ "max_abs": error.abs().max().item(),
301
+ }
302
+ del fp_weight, linear, quantized, restored, quant_weight, error
303
+ return report
304
+
305
+ @staticmethod
306
+ def _name_matches(full_name, patterns):
307
+ if patterns is None:
308
+ return False
309
+ if full_name in patterns:
310
+ return True
311
+ return any(full_name.endswith(f".{pattern}") for pattern in patterns)
312
+
313
+ def _should_quantize(self, full_name, module):
314
+ if not isinstance(module, torch.nn.Linear):
315
+ return False
316
+ if self.is_quantized_linear(module):
317
+ return False
318
+ if self.target_modules is not None and not self._name_matches(full_name, self.target_modules):
319
+ return False
320
+ if self._name_matches(full_name, self.exclude_modules):
321
+ return False
322
+ return True
323
+
324
+ def _replace_target_linears(self, model, transform):
325
+ replaced = [
326
+ full_name
327
+ for full_name, module in model.named_modules()
328
+ if full_name != "" and self._should_quantize(full_name, module)
329
+ ]
330
+ for full_name in replaced:
331
+ parent_name, _, leaf_name = full_name.rpartition(".")
332
+ parent = model.get_submodule(parent_name) if parent_name else model
333
+ setattr(parent, leaf_name, transform(getattr(parent, leaf_name)))
334
+ return replaced
335
+
336
+ def _dequantize_linears(self, model, compute_dtype, compute_device=None, model_device=None):
337
+ restored = [
338
+ full_name
339
+ for full_name, module in model.named_modules()
340
+ if full_name != "" and self.backend.is_quantized_linear(module)
341
+ ]
342
+ for full_name in restored:
343
+ parent_name, _, leaf_name = full_name.rpartition(".")
344
+ parent = model.get_submodule(parent_name) if parent_name else model
345
+ restored_linear = self.backend.dequantize_to_linear(
346
+ getattr(parent, leaf_name), compute_dtype, compute_device=compute_device, model_device=model_device
347
+ )
348
+ setattr(parent, leaf_name, restored_linear)
349
+ return restored
350
+
351
+
352
+ @dataclass
353
+ class MixedQuantizeConfig:
354
+ """
355
+ Combines multiple `QuantizeConfig` passes, one per disjoint layer set, behind the
356
+ same interface as a single `QuantizeConfig`. Mixed quantization assigns different
357
+ methods to different layers, e.g. nf4 for attention/MLP and int8 for the
358
+ quantization-sensitive modulation layers:
359
+
360
+ mod_layers = ["img_mod.1", "txt_mod.1", ...]
361
+ cfg = MixedQuantizeConfig(configs=[
362
+ QuantizeConfig(method="bitsandbytes_nf4", exclude_modules=mod_layers),
363
+ QuantizeConfig(method="torchao_int8_w8a16", target_modules=mod_layers),
364
+ ])
365
+ cfg.quantize_model(model, compute_device="cuda")
366
+
367
+ The passes run in `configs` order. Their matched layer sets must be pairwise
368
+ disjoint; `quantize_model` and `prepare_for_prequantized_load` verify this on the
369
+ fp model and raise before touching it.
370
+
371
+ Fields:
372
+ configs: list of `QuantizeConfig`, one per layer set; all must share the same
373
+ `mode`, and their `load_prequantized` must stay False.
374
+ load_prequantized: set on this wrapper (not on the children) to load a mixed
375
+ pre-quantized checkpoint.
376
+ """
377
+ configs: list = None
378
+ load_prequantized: bool = False
379
+
380
+ def __post_init__(self):
381
+ if not self.configs:
382
+ raise ValueError("`MixedQuantizeConfig.configs` requires at least one `QuantizeConfig`.")
383
+ for config in self.configs:
384
+ if not isinstance(config, QuantizeConfig):
385
+ raise ValueError(f"`MixedQuantizeConfig.configs` should contain `QuantizeConfig` instances, but got `{type(config).__name__}`.")
386
+ if config.load_prequantized:
387
+ raise ValueError(
388
+ "Set `load_prequantized=True` on `MixedQuantizeConfig` itself, not on its "
389
+ "child configs; the children only describe method + layer set."
390
+ )
391
+ modes = {config.mode for config in self.configs}
392
+ if len(modes) > 1:
393
+ raise ValueError(f"All configs in `MixedQuantizeConfig` should share the same `mode`, but got {sorted(modes)}.")
394
+ self._ownership = {}
395
+
396
+ @property
397
+ def method(self):
398
+ return " + ".join(config.method for config in self.configs)
399
+
400
+ @property
401
+ def mode(self):
402
+ return self.configs[0].mode
403
+
404
+ def quantize_model(self, model: torch.nn.Module, compute_device=None, model_device=None):
405
+ """
406
+ Run every config's quantization pass in order, after verifying their layer sets
407
+ are disjoint. Does nothing when `load_prequantized` is set.
408
+
409
+ Parameters:
410
+ model: the fp model whose targeted layers are replaced by quantized ones.
411
+ compute_device: where quantization runs; `None` quantizes each layer in place.
412
+ model_device: where each quantized layer is stored afterwards; `None` leaves it
413
+ on `compute_device`.
414
+ """
415
+ if self.load_prequantized:
416
+ return model
417
+ self._build_ownership(model)
418
+ for config in self.configs:
419
+ config.quantize_model(model, compute_device=compute_device, model_device=model_device)
420
+ return model
421
+
422
+ def dequantize_model(self, model: torch.nn.Module, compute_dtype: torch.dtype = torch.bfloat16, compute_device=None, model_device=None):
423
+ """
424
+ Replace every quantized Linear of every config's backend by a plain fp `nn.Linear`.
425
+ Does nothing unless `mode` is `"dequant_once"`.
426
+
427
+ Parameters:
428
+ model: the quantized model to restore in place.
429
+ compute_dtype: dtype of the restored fp weights.
430
+ compute_device: where dequantization runs; `None` restores each layer in place.
431
+ model_device: where each restored layer is stored afterwards; `None` leaves it
432
+ on `compute_device`.
433
+ """
434
+ if self.mode != "dequant_once":
435
+ return model
436
+ for config in self.configs:
437
+ config.dequantize_model(model, compute_dtype=compute_dtype, compute_device=compute_device, model_device=model_device)
438
+ return model
439
+
440
+ def is_quantized_linear(self, module) -> bool:
441
+ """Whether `module` is a quantized Linear of any config's backend."""
442
+ return any(config.is_quantized_linear(module) for config in self.configs)
443
+
444
+ def build_quantized_shell(self, module, compute_dtype: torch.dtype, layer_name: str = None):
445
+ """
446
+ Build an empty quantized Linear for `module` using the config that owns it.
447
+
448
+ Parameters:
449
+ module: the quantized layer whose shape and bias presence are mirrored.
450
+ compute_dtype: dtype the shell dequantizes to at forward time.
451
+ layer_name: its dotted name inside the model; required to pick the right config
452
+ when several configs share one backend, since their quantized Linears are
453
+ then the same class.
454
+ """
455
+ return self._owning_config(module, layer_name).build_quantized_shell(module, compute_dtype)
456
+
457
+ def _owning_config(self, module, layer_name=None):
458
+ config = self._ownership.get(layer_name)
459
+ if config is not None:
460
+ return config
461
+ for config in self.configs:
462
+ if config.is_quantized_linear(module):
463
+ return config
464
+ raise ValueError(
465
+ f"`{type(module).__name__}` is not a quantized Linear of any config in this "
466
+ f"`MixedQuantizeConfig` (methods: {self.method})."
467
+ )
468
+
469
+ def prepare_for_prequantized_load(self, model: torch.nn.Module, compute_dtype: torch.dtype = torch.bfloat16):
470
+ """
471
+ Replace each config's targeted Linears by that backend's shells, after verifying
472
+ the layer sets are disjoint.
473
+
474
+ Parameters:
475
+ model: the freshly constructed model whose targeted layers become shells.
476
+ compute_dtype: dtype the quantized layers dequantize to at forward time.
477
+ """
478
+ self._build_ownership(model)
479
+ for config in self.configs:
480
+ config.prepare_for_prequantized_load(model, compute_dtype=compute_dtype)
481
+ return model
482
+
483
+ def unflatten_state_dict(self, state_dict: dict, metadata: dict):
484
+ """
485
+ Rebuild composite quantized tensors from a flat (safetensors) state dict.
486
+ """
487
+ if not self._ownership:
488
+ for config in self._distinct_backend_configs():
489
+ state_dict = config.unflatten_state_dict(state_dict, metadata)
490
+ return state_dict
491
+ rebuilt = {}
492
+ for config, keys in self._grouped_by_backend(state_dict):
493
+ rebuilt.update(config.unflatten_state_dict(keys, metadata) if config else keys)
494
+ return rebuilt
495
+
496
+ def flatten_state_dict(self, state_dict: dict):
497
+ """
498
+ Flatten the mixed model's state dict into plain tensors and string-only metadata.
499
+ Returns (state_dict, metadata), ready for `safetensors.torch.save_file(tensors,
500
+ path, metadata=metadata)`.
501
+ """
502
+ for config in self.configs:
503
+ if not config.backend.capabilities().get("is_serializable", False):
504
+ raise NotImplementedError(
505
+ f"Backend `{config.backend.name}` (method `{config.method}`) does not declare "
506
+ 'serialization support (`capabilities()["is_serializable"]`), so the mixed '
507
+ "state dict cannot be flattened for saving."
508
+ )
509
+ merged_metadata = {}
510
+ if not self._ownership:
511
+ flattened = state_dict
512
+ for config in self._distinct_backend_configs():
513
+ flattened, metadata = config.backend.flatten_state_dict(flattened)
514
+ merged_metadata.update(metadata)
515
+ else:
516
+ flattened = {}
517
+ for config, keys in self._grouped_by_backend(state_dict):
518
+ if config is None:
519
+ flattened.update(keys)
520
+ continue
521
+ tensors, metadata = config.backend.flatten_state_dict(keys)
522
+ flattened.update(tensors)
523
+ merged_metadata.update(metadata)
524
+ tensors = {key: value.contiguous() for key, value in flattened.items()}
525
+ metadata = {"format": "pt", **{key: value if isinstance(value, str) else str(value) for key, value in merged_metadata.items()}}
526
+ return tensors, metadata
527
+
528
+ def measure_quantization_error(self, model: torch.nn.Module, compute_device="cuda", verbose=True) -> list:
529
+ """
530
+ Measure the weight error of the mixed quantization.
531
+ """
532
+ reports = []
533
+ for config in self.configs:
534
+ reports.extend(config.measure_quantization_error(model, compute_device=compute_device, verbose=False))
535
+ reports.sort(key=lambda report: report["relative_error"], reverse=True)
536
+ if verbose:
537
+ print(_format_error_report(reports))
538
+ return reports
539
+
540
+ def _distinct_backend_configs(self):
541
+ seen = set()
542
+ distinct = []
543
+ for config in self.configs:
544
+ if config.backend.name not in seen:
545
+ seen.add(config.backend.name)
546
+ distinct.append(config)
547
+ return distinct
548
+
549
+ def _grouped_by_backend(self, state_dict):
550
+ groups = {}
551
+ for key, value in state_dict.items():
552
+ parts = key.split(".")
553
+ config = next((owner for end in range(len(parts) - 1, 0, -1)
554
+ if (owner := self._ownership.get(".".join(parts[:end]))) is not None), None)
555
+ groups.setdefault(config.backend.name if config else None, (config, {}))[1][key] = value
556
+ return groups.values()
557
+
558
+ def _build_ownership(self, model):
559
+ ownership = {}
560
+ for index, config in enumerate(self.configs):
561
+ names = [name for name, module in model.named_modules() if name and config._should_quantize(name, module)]
562
+ overlap = sorted(set(names) & ownership.keys())
563
+ if overlap:
564
+ owner = self.configs.index(ownership[overlap[0]])
565
+ raise ValueError(
566
+ f"Configs {owner} (`{self.configs[owner].method}`) and {index} (`{config.method}`) "
567
+ f"in `MixedQuantizeConfig` both match {len(overlap)} layers (e.g. {', '.join(overlap[:5])}). "
568
+ )
569
+ ownership.update({name: config for name in names})
570
+ self._ownership.update(ownership)
diffsynth/core/vram/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .initialization import skip_model_initialization
2
+ from .layers import *
diffsynth/core/vram/disk_map.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from safetensors import safe_open
2
+ import torch, os
3
+
4
+
5
+ class SafetensorsCompatibleTensor:
6
+ def __init__(self, tensor):
7
+ self.tensor = tensor
8
+
9
+ def get_shape(self):
10
+ return list(self.tensor.shape)
11
+
12
+
13
+ class SafetensorsCompatibleBinaryLoader:
14
+ def __init__(self, path, device):
15
+ print("Detected non-safetensors files, which may cause slower loading. It's recommended to convert it to a safetensors file.")
16
+ self.state_dict = torch.load(path, weights_only=True, map_location=device)
17
+
18
+ def keys(self):
19
+ return self.state_dict.keys()
20
+
21
+ def get_tensor(self, name):
22
+ return self.state_dict[name]
23
+
24
+ def get_slice(self, name):
25
+ return SafetensorsCompatibleTensor(self.state_dict[name])
26
+
27
+
28
+ class DiskMap:
29
+
30
+ def __init__(self, path, device, torch_dtype=None, state_dict_converter=None, buffer_size=10**9):
31
+ self.path = path if isinstance(path, list) else [path]
32
+ self.device = device
33
+ self.torch_dtype = torch_dtype
34
+ if os.environ.get('DIFFSYNTH_DISK_MAP_BUFFER_SIZE') is not None:
35
+ self.buffer_size = int(os.environ.get('DIFFSYNTH_DISK_MAP_BUFFER_SIZE'))
36
+ else:
37
+ self.buffer_size = buffer_size
38
+ self.files = []
39
+ self.flush_files()
40
+ self.name_map = {}
41
+ for file_id, file in enumerate(self.files):
42
+ for name in file.keys():
43
+ self.name_map[name] = file_id
44
+ self.rename_dict = self.fetch_rename_dict(state_dict_converter)
45
+
46
+ def flush_files(self):
47
+ if len(self.files) == 0:
48
+ for path in self.path:
49
+ if path.endswith(".safetensors"):
50
+ self.files.append(safe_open(path, framework="pt", device=str(self.device)))
51
+ else:
52
+ self.files.append(SafetensorsCompatibleBinaryLoader(path, device=self.device))
53
+ else:
54
+ for i, path in enumerate(self.path):
55
+ if path.endswith(".safetensors"):
56
+ self.files[i] = safe_open(path, framework="pt", device=str(self.device))
57
+ self.num_params = 0
58
+
59
+ def __getitem__(self, name):
60
+ if self.rename_dict is not None: name = self.rename_dict[name]
61
+ file_id = self.name_map[name]
62
+ param = self.files[file_id].get_tensor(name)
63
+ if self.torch_dtype is not None and isinstance(param, torch.Tensor):
64
+ param = param.to(self.torch_dtype)
65
+ if isinstance(param, torch.Tensor) and param.device.type == "cpu":
66
+ param = param.clone()
67
+ if isinstance(param, torch.Tensor):
68
+ self.num_params += param.numel()
69
+ if self.num_params > self.buffer_size:
70
+ self.flush_files()
71
+ return param
72
+
73
+ def fetch_rename_dict(self, state_dict_converter):
74
+ if state_dict_converter is None:
75
+ return None
76
+ state_dict = {}
77
+ for file in self.files:
78
+ for name in file.keys():
79
+ state_dict[name] = name
80
+ state_dict = state_dict_converter(state_dict)
81
+ return state_dict
82
+
83
+ def __iter__(self):
84
+ if self.rename_dict is not None:
85
+ return self.rename_dict.__iter__()
86
+ else:
87
+ return self.name_map.__iter__()
88
+
89
+ def __contains__(self, x):
90
+ if self.rename_dict is not None:
91
+ return x in self.rename_dict
92
+ else:
93
+ return x in self.name_map
diffsynth/core/vram/initialization.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from contextlib import contextmanager
3
+
4
+
5
+ @contextmanager
6
+ def skip_model_initialization(device=torch.device("meta")):
7
+
8
+ def register_empty_parameter(module, name, param):
9
+ old_register_parameter(module, name, param)
10
+ if param is not None:
11
+ param_cls = type(module._parameters[name])
12
+ kwargs = module._parameters[name].__dict__
13
+ kwargs["requires_grad"] = param.requires_grad
14
+ module._parameters[name] = param_cls(module._parameters[name].to(device), **kwargs)
15
+
16
+ old_register_parameter = torch.nn.Module.register_parameter
17
+ torch.nn.Module.register_parameter = register_empty_parameter
18
+ try:
19
+ yield
20
+ finally:
21
+ torch.nn.Module.register_parameter = old_register_parameter
diffsynth/core/vram/layers.py ADDED
@@ -0,0 +1,620 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch, copy
2
+ from typing import Union
3
+ from .initialization import skip_model_initialization
4
+ from .disk_map import DiskMap
5
+ from ..device import parse_device_type, get_device_name, IS_NPU_AVAILABLE
6
+
7
+
8
+ class AutoTorchModule(torch.nn.Module):
9
+
10
+ def __init__(
11
+ self,
12
+ offload_dtype: torch.dtype = None,
13
+ offload_device: Union[str, torch.device] = None,
14
+ onload_dtype: torch.dtype = None,
15
+ onload_device: Union[str, torch.device] = None,
16
+ preparing_dtype: torch.dtype = None,
17
+ preparing_device: Union[str, torch.device] = None,
18
+ computation_dtype: torch.dtype = None,
19
+ computation_device: Union[str, torch.device] = None,
20
+ vram_limit: float = None,
21
+ ):
22
+ super().__init__()
23
+ self.set_dtype_and_device(
24
+ offload_dtype,
25
+ offload_device,
26
+ onload_dtype,
27
+ onload_device,
28
+ preparing_dtype,
29
+ preparing_device,
30
+ computation_dtype,
31
+ computation_device,
32
+ vram_limit,
33
+ )
34
+ self.state = 0
35
+ self.name = ""
36
+ self.computation_device_type = parse_device_type(self.computation_device)
37
+
38
+ def set_dtype_and_device(
39
+ self,
40
+ offload_dtype: torch.dtype = None,
41
+ offload_device: Union[str, torch.device] = None,
42
+ onload_dtype: torch.dtype = None,
43
+ onload_device: Union[str, torch.device] = None,
44
+ preparing_dtype: torch.dtype = None,
45
+ preparing_device: Union[str, torch.device] = None,
46
+ computation_dtype: torch.dtype = None,
47
+ computation_device: Union[str, torch.device] = None,
48
+ vram_limit: float = None,
49
+ ):
50
+ self.offload_dtype = offload_dtype or computation_dtype
51
+ self.offload_device = offload_device or ("disk" if self.offload_dtype == "disk" else computation_device)
52
+ self.onload_dtype = onload_dtype or computation_dtype
53
+ self.onload_device = onload_device or ("disk" if self.onload_dtype == "disk" else computation_device)
54
+ self.preparing_dtype = preparing_dtype or computation_dtype
55
+ self.preparing_device = preparing_device or ("disk" if self.preparing_dtype == "disk" else computation_device)
56
+ self.computation_dtype = computation_dtype
57
+ self.computation_device = computation_device
58
+ self.vram_limit = vram_limit
59
+
60
+ def cast_to(self, weight, dtype, device):
61
+ r = torch.empty_like(weight, dtype=dtype, device=device)
62
+ r.copy_(weight)
63
+ return r
64
+
65
+ def check_free_vram(self):
66
+ if self.computation_device_type == "mps":
67
+ allocated = torch.mps.current_allocated_memory()
68
+ recommended_max = torch.mps.recommended_max_memory()
69
+ gpu_mem_state = (recommended_max - allocated, recommended_max)
70
+ else:
71
+ device = self.computation_device if not IS_NPU_AVAILABLE else get_device_name()
72
+ gpu_mem_state = getattr(torch, self.computation_device_type).mem_get_info(device)
73
+ used_memory = (gpu_mem_state[1] - gpu_mem_state[0]) / (1024**3)
74
+ return used_memory < self.vram_limit
75
+
76
+ def offload(self):
77
+ if self.state != 0:
78
+ self.to(dtype=self.offload_dtype, device=self.offload_device)
79
+ self.state = 0
80
+
81
+ def onload(self):
82
+ if self.state != 1:
83
+ self.to(dtype=self.onload_dtype, device=self.onload_device)
84
+ self.state = 1
85
+
86
+ def param_name(self, name):
87
+ if self.name == "":
88
+ return name
89
+ else:
90
+ return self.name + "." + name
91
+
92
+
93
+ class AutoWrappedModule(AutoTorchModule):
94
+
95
+ def __init__(
96
+ self,
97
+ module: torch.nn.Module,
98
+ offload_dtype: torch.dtype = None,
99
+ offload_device: Union[str, torch.device] = None,
100
+ onload_dtype: torch.dtype = None,
101
+ onload_device: Union[str, torch.device] = None,
102
+ preparing_dtype: torch.dtype = None,
103
+ preparing_device: Union[str, torch.device] = None,
104
+ computation_dtype: torch.dtype = None,
105
+ computation_device: Union[str, torch.device] = None,
106
+ vram_limit: float = None,
107
+ name: str = "",
108
+ disk_map: DiskMap = None,
109
+ **kwargs
110
+ ):
111
+ super().__init__(
112
+ offload_dtype,
113
+ offload_device,
114
+ onload_dtype,
115
+ onload_device,
116
+ preparing_dtype,
117
+ preparing_device,
118
+ computation_dtype,
119
+ computation_device,
120
+ vram_limit,
121
+ )
122
+ self.module = module
123
+ if offload_dtype == "disk":
124
+ self.name = name
125
+ self.disk_map = disk_map
126
+ self.required_params = [name for name, _ in self.module.named_parameters()]
127
+ self.disk_offload = True
128
+ else:
129
+ self.disk_offload = False
130
+
131
+ def load_from_disk(self, torch_dtype, device, copy_module=False):
132
+ if copy_module:
133
+ module = copy.deepcopy(self.module)
134
+ else:
135
+ module = self.module
136
+ state_dict = {}
137
+ for name in self.required_params:
138
+ param = self.disk_map[self.param_name(name)]
139
+ param = param.to(dtype=torch_dtype, device=device)
140
+ state_dict[name] = param
141
+ module.load_state_dict(state_dict, assign=True)
142
+ module.to(dtype=torch_dtype, device=device)
143
+ return module
144
+
145
+ def offload_to_disk(self, model: torch.nn.Module):
146
+ for buf in model.buffers():
147
+ # If there are some parameters are registed in buffers (not in state dict),
148
+ # We cannot offload the model.
149
+ for children in model.children():
150
+ self.offload_to_disk(children)
151
+ break
152
+ else:
153
+ model.to("meta")
154
+
155
+ def offload(self):
156
+ # offload / onload / preparing -> offload
157
+ if self.state != 0:
158
+ if self.disk_offload:
159
+ self.offload_to_disk(self.module)
160
+ else:
161
+ self.to(dtype=self.offload_dtype, device=self.offload_device)
162
+ self.state = 0
163
+
164
+ def onload(self):
165
+ # offload / onload / preparing -> onload
166
+ if self.state < 1:
167
+ if self.disk_offload and self.onload_device != "disk" and self.offload_device == "disk":
168
+ self.load_from_disk(self.onload_dtype, self.onload_device)
169
+ elif self.onload_device != "disk":
170
+ self.to(dtype=self.onload_dtype, device=self.onload_device)
171
+ self.state = 1
172
+
173
+ def preparing(self):
174
+ # onload / preparing -> preparing
175
+ if self.state != 2:
176
+ if self.disk_offload and self.preparing_device != "disk" and self.onload_device == "disk":
177
+ self.load_from_disk(self.preparing_dtype, self.preparing_device)
178
+ elif self.preparing_device != "disk":
179
+ self.to(dtype=self.preparing_dtype, device=self.preparing_device)
180
+ self.state = 2
181
+
182
+ def cast_to(self, module, dtype, device):
183
+ return copy.deepcopy(module).to(dtype=dtype, device=device)
184
+
185
+ def computation(self):
186
+ # onload / preparing -> computation (temporary)
187
+ if self.state == 2:
188
+ torch_dtype, device = self.preparing_dtype, self.preparing_device
189
+ else:
190
+ torch_dtype, device = self.onload_dtype, self.onload_device
191
+ if torch_dtype == self.computation_dtype and device == self.computation_device:
192
+ module = self.module
193
+ elif self.disk_offload and device == "disk":
194
+ module = self.load_from_disk(self.computation_dtype, self.computation_device, copy_module=True)
195
+ else:
196
+ module = self.cast_to(self.module, dtype=self.computation_dtype, device=self.computation_device)
197
+ return module
198
+
199
+ def forward(self, *args, **kwargs):
200
+ if self.state == 1 and (self.vram_limit is None or self.check_free_vram()):
201
+ self.preparing()
202
+ module = self.computation()
203
+ return module(*args, **kwargs)
204
+
205
+ def __getattr__(self, name):
206
+ if name in self.__dict__ or name == "module":
207
+ return super().__getattr__(name)
208
+ else:
209
+ return getattr(self.module, name)
210
+
211
+
212
+ class AutoWrappedNonRecurseModule(AutoWrappedModule):
213
+
214
+ def __init__(
215
+ self,
216
+ module: torch.nn.Module,
217
+ offload_dtype: torch.dtype = None,
218
+ offload_device: Union[str, torch.device] = None,
219
+ onload_dtype: torch.dtype = None,
220
+ onload_device: Union[str, torch.device] = None,
221
+ preparing_dtype: torch.dtype = None,
222
+ preparing_device: Union[str, torch.device] = None,
223
+ computation_dtype: torch.dtype = None,
224
+ computation_device: Union[str, torch.device] = None,
225
+ vram_limit: float = None,
226
+ name: str = "",
227
+ disk_map: DiskMap = None,
228
+ **kwargs
229
+ ):
230
+ super().__init__(
231
+ module,
232
+ offload_dtype,
233
+ offload_device,
234
+ onload_dtype,
235
+ onload_device,
236
+ preparing_dtype,
237
+ preparing_device,
238
+ computation_dtype,
239
+ computation_device,
240
+ vram_limit,
241
+ name,
242
+ disk_map,
243
+ **kwargs
244
+ )
245
+ if self.disk_offload:
246
+ self.required_params = [name for name, _ in self.module.named_parameters(recurse=False)]
247
+
248
+ def load_from_disk(self, torch_dtype, device, copy_module=False):
249
+ if copy_module:
250
+ module = copy.deepcopy(self.module)
251
+ else:
252
+ module = self.module
253
+ state_dict = {}
254
+ for name in self.required_params:
255
+ param = self.disk_map[self.param_name(name)]
256
+ param = param.to(dtype=torch_dtype, device=device)
257
+ state_dict[name] = param
258
+ module.load_state_dict(state_dict, assign=True, strict=False)
259
+ return module
260
+
261
+ def offload_to_disk(self, model: torch.nn.Module):
262
+ for name in self.required_params:
263
+ getattr(self, name).to("meta")
264
+
265
+ def cast_to(self, module, dtype, device):
266
+ # Parameter casting is implemented in the model architecture.
267
+ return module
268
+
269
+ def __getattr__(self, name):
270
+ if name in self.__dict__ or name == "module":
271
+ return super().__getattr__(name)
272
+ else:
273
+ return getattr(self.module, name)
274
+
275
+
276
+ class LoRAHotLoadMixin:
277
+ def init_lora_hotload(self):
278
+ self.lora_A_weights = []
279
+ self.lora_B_weights = []
280
+ self.lora_merger = None
281
+
282
+ def lora_forward(self, x, out):
283
+ if self.lora_merger is None:
284
+ for lora_A, lora_B in zip(self.lora_A_weights, self.lora_B_weights):
285
+ out = out + x @ lora_A.T.to(device=x.device, dtype=x.dtype) @ lora_B.T.to(device=x.device, dtype=x.dtype)
286
+ else:
287
+ lora_output = []
288
+ for lora_A, lora_B in zip(self.lora_A_weights, self.lora_B_weights):
289
+ lora_output.append(x @ lora_A.T @ lora_B.T)
290
+ lora_output = torch.stack(lora_output)
291
+ out = self.lora_merger(out, lora_output)
292
+ return out
293
+
294
+
295
+ class AutoWrappedLinear(torch.nn.Linear, AutoTorchModule, LoRAHotLoadMixin):
296
+ def __init__(
297
+ self,
298
+ module: torch.nn.Linear,
299
+ offload_dtype: torch.dtype = None,
300
+ offload_device: Union[str, torch.device] = None,
301
+ onload_dtype: torch.dtype = None,
302
+ onload_device: Union[str, torch.device] = None,
303
+ preparing_dtype: torch.dtype = None,
304
+ preparing_device: Union[str, torch.device] = None,
305
+ computation_dtype: torch.dtype = None,
306
+ computation_device: Union[str, torch.device] = None,
307
+ vram_limit: float = None,
308
+ name: str = "",
309
+ disk_map: DiskMap = None,
310
+ **kwargs
311
+ ):
312
+ with skip_model_initialization():
313
+ super().__init__(
314
+ in_features=module.in_features,
315
+ out_features=module.out_features,
316
+ bias=module.bias is not None,
317
+ )
318
+ self.set_dtype_and_device(
319
+ offload_dtype,
320
+ offload_device,
321
+ onload_dtype,
322
+ onload_device,
323
+ preparing_dtype,
324
+ preparing_device,
325
+ computation_dtype,
326
+ computation_device,
327
+ vram_limit,
328
+ )
329
+ self.weight = module.weight
330
+ self.bias = module.bias
331
+ self.state = 0
332
+ self.name = name
333
+ self.init_lora_hotload()
334
+ self.enable_fp8 = computation_dtype in [torch.float8_e4m3fn, torch.float8_e4m3fnuz]
335
+ self.computation_device_type = parse_device_type(self.computation_device)
336
+
337
+ if offload_dtype == "disk":
338
+ self.disk_map = disk_map
339
+ self.disk_offload = True
340
+ else:
341
+ self.disk_offload = False
342
+
343
+ def fp8_linear(
344
+ self,
345
+ input: torch.Tensor,
346
+ weight: torch.Tensor,
347
+ bias: torch.Tensor = None,
348
+ ) -> torch.Tensor:
349
+ device = input.device
350
+ origin_dtype = input.dtype
351
+ origin_shape = input.shape
352
+ input = input.reshape(-1, origin_shape[-1])
353
+
354
+ x_max = torch.max(torch.abs(input), dim=-1, keepdim=True).values
355
+ # Use the maximum finite value defined by the selected FP8 dtype.
356
+ fp8_max = torch.finfo(self.computation_dtype).max
357
+ scale_a = torch.clamp(x_max / fp8_max, min=1.0).float().to(device=device)
358
+ scale_b = torch.ones((weight.shape[0], 1)).to(device=device)
359
+ input = input / (scale_a + 1e-8)
360
+ input = input.to(self.computation_dtype)
361
+ weight = weight.to(self.computation_dtype)
362
+ if bias is not None:
363
+ bias = bias.to(torch.bfloat16)
364
+
365
+ result = torch._scaled_mm(
366
+ input.to(self.computation_dtype),
367
+ weight.T,
368
+ scale_a=scale_a,
369
+ scale_b=scale_b.T,
370
+ bias=bias,
371
+ out_dtype=origin_dtype,
372
+ )
373
+ new_shape = origin_shape[:-1] + result.shape[-1:]
374
+ result = result.reshape(new_shape)
375
+ return result
376
+
377
+ def load_from_disk(self, torch_dtype, device, assign=True):
378
+ weight = self.disk_map[self.name + ".weight"].to(dtype=torch_dtype, device=device)
379
+ bias = None if self.bias is None else self.disk_map[self.name + ".bias"].to(dtype=torch_dtype, device=device)
380
+ if assign:
381
+ state_dict = {"weight": weight}
382
+ if bias is not None: state_dict["bias"] = bias
383
+ self.load_state_dict(state_dict, assign=True)
384
+ return weight, bias
385
+
386
+ def offload(self):
387
+ # offload / onload / preparing -> offload
388
+ if self.state != 0:
389
+ if self.disk_offload:
390
+ self.to("meta")
391
+ else:
392
+ self.to(dtype=self.offload_dtype, device=self.offload_device)
393
+ self.state = 0
394
+
395
+ def onload(self):
396
+ # offload / onload / preparing -> onload
397
+ if self.state < 1:
398
+ if self.disk_offload and self.onload_device != "disk" and self.offload_device == "disk":
399
+ self.load_from_disk(self.onload_dtype, self.onload_device)
400
+ elif self.onload_device != "disk":
401
+ self.to(dtype=self.onload_dtype, device=self.onload_device)
402
+ self.state = 1
403
+
404
+ def preparing(self):
405
+ # onload / preparing -> preparing
406
+ if self.state != 2:
407
+ if self.disk_offload and self.preparing_device != "disk" and self.onload_device == "disk":
408
+ self.load_from_disk(self.preparing_dtype, self.preparing_device)
409
+ elif self.preparing_device != "disk":
410
+ self.to(dtype=self.preparing_dtype, device=self.preparing_device)
411
+ self.state = 2
412
+
413
+ def computation(self):
414
+ # onload / preparing -> computation (temporary)
415
+ if self.state == 2:
416
+ torch_dtype, device = self.preparing_dtype, self.preparing_device
417
+ else:
418
+ torch_dtype, device = self.onload_dtype, self.onload_device
419
+ if torch_dtype == self.computation_dtype and device == self.computation_device:
420
+ weight, bias = self.weight, self.bias
421
+ elif self.disk_offload and device == "disk":
422
+ weight, bias = self.load_from_disk(self.computation_dtype, self.computation_device, assign=False)
423
+ else:
424
+ weight = self.cast_to(self.weight, self.computation_dtype, self.computation_device)
425
+ bias = None if self.bias is None else self.cast_to(self.bias, self.computation_dtype, self.computation_device)
426
+ return weight, bias
427
+
428
+ def linear_forward(self, x, weight, bias):
429
+ if self.enable_fp8:
430
+ out = self.fp8_linear(x, weight, bias)
431
+ else:
432
+ out = torch.nn.functional.linear(x, weight, bias)
433
+ return out
434
+
435
+ def forward(self, x, *args, **kwargs):
436
+ if self.state == 1 and (self.vram_limit is None or self.check_free_vram()):
437
+ self.preparing()
438
+ weight, bias = self.computation()
439
+ out = self.linear_forward(x, weight, bias)
440
+ if len(self.lora_A_weights) > 0:
441
+ out = self.lora_forward(x, out)
442
+ return out
443
+
444
+
445
+ class AutoWrappedQuantizedModule(torch.nn.Linear, AutoTorchModule, LoRAHotLoadMixin):
446
+ def __init__(
447
+ self,
448
+ module: torch.nn.Module,
449
+ offload_dtype: torch.dtype = None,
450
+ offload_device: Union[str, torch.device] = None,
451
+ onload_dtype: torch.dtype = None,
452
+ onload_device: Union[str, torch.device] = None,
453
+ preparing_dtype: torch.dtype = None,
454
+ preparing_device: Union[str, torch.device] = None,
455
+ computation_dtype: torch.dtype = None,
456
+ computation_device: Union[str, torch.device] = None,
457
+ vram_limit: float = None,
458
+ name: str = "",
459
+ disk_map: DiskMap = None,
460
+ quantize=None,
461
+ metadata: dict = None,
462
+ **kwargs
463
+ ):
464
+ disk_offload = "disk" in (offload_dtype, offload_device, onload_dtype, onload_device)
465
+ if disk_offload and (disk_map is None or quantize is None):
466
+ raise ValueError(
467
+ "Disk offload for quantized layers requires both `disk_map` and `quantize`, "
468
+ "so each layer can rebuild its packed weight and quant state lazily."
469
+ )
470
+ AutoTorchModule.__init__(
471
+ self,
472
+ offload_dtype,
473
+ offload_device,
474
+ onload_dtype,
475
+ onload_device,
476
+ preparing_dtype,
477
+ preparing_device,
478
+ computation_dtype,
479
+ computation_device,
480
+ vram_limit,
481
+ )
482
+ self.in_features = module.in_features
483
+ self.out_features = module.out_features
484
+ self.module = module
485
+ self.name = name
486
+ self.disk_offload = disk_offload
487
+ self.disk_map = disk_map
488
+ self.quantize = quantize
489
+ self.metadata = metadata
490
+ self._required_keys = None
491
+ self.init_lora_hotload()
492
+
493
+ def _disk_required_keys(self):
494
+ if self._required_keys is None:
495
+ self._required_keys = [key for key in self.disk_map if key.startswith(self.name + ".")]
496
+ return self._required_keys
497
+
498
+ def _load_from_disk(self, device, target=None):
499
+ module = self.module if target is None else target
500
+ prefix = self.name + "."
501
+ subdict = {key: self.disk_map[key] for key in self._disk_required_keys()}
502
+ rebuilt = self.quantize.unflatten_state_dict(subdict, self.metadata or {})
503
+ state = {key[len(prefix):]: value for key, value in rebuilt.items()}
504
+ module.load_state_dict(state, assign=True)
505
+ module.to(device=device)
506
+ return module
507
+
508
+ def _module_device(self):
509
+ tensor = next(self.module.parameters(), None)
510
+ if tensor is None:
511
+ tensor = next(self.module.buffers(), None)
512
+ return tensor.device
513
+
514
+ def offload(self):
515
+ if self.state != 0:
516
+ if self.disk_offload:
517
+ self.module = self.quantize.build_quantized_shell(self.module, self.computation_dtype, layer_name=self.name)
518
+ else:
519
+ self.module.to(device=self.offload_device)
520
+ self.state = 0
521
+
522
+ def onload(self):
523
+ if self.state < 1:
524
+ if self.disk_offload and self.onload_device != "disk" and self.offload_device == "disk":
525
+ self._load_from_disk(self.onload_device)
526
+ elif self.onload_device != "disk":
527
+ self.module.to(device=self.onload_device)
528
+ self.state = 1
529
+
530
+ def preparing(self):
531
+ if self.state != 2:
532
+ if self.disk_offload and self.preparing_device != "disk" and self.onload_device == "disk":
533
+ self._load_from_disk(self.preparing_device)
534
+ elif self.preparing_device != "disk":
535
+ self.module.to(device=self.preparing_device)
536
+ self.state = 2
537
+
538
+ def computation_module(self):
539
+ device = self.preparing_device if self.state == 2 else self.onload_device
540
+ if device == self.computation_device:
541
+ return self.module
542
+ if self.disk_offload and device == "disk":
543
+ transient = self.quantize.build_quantized_shell(self.module, self.computation_dtype, layer_name=self.name)
544
+ return self._load_from_disk(self.computation_device, target=transient)
545
+ return copy.deepcopy(self.module).to(device=self.computation_device)
546
+
547
+ def forward(self, x, *args, **kwargs):
548
+ if self.state == 1 and (self.vram_limit is None or self.check_free_vram()):
549
+ self.preparing()
550
+ module = self.computation_module()
551
+ out = module(x, *args, **kwargs)
552
+ if len(self.lora_A_weights) > 0:
553
+ out = self.lora_forward(x, out)
554
+ return out
555
+
556
+ def __getattr__(self, name):
557
+ if name in self.__dict__ or name == "module":
558
+ return super().__getattr__(name)
559
+ else:
560
+ return getattr(self.module, name)
561
+
562
+
563
+ def _materialize_root_params_from_disk(model: torch.nn.Module, disk_map: DiskMap):
564
+ for name, param in list(model.named_parameters(recurse=False)):
565
+ if param is None or not param.is_meta or name not in disk_map:
566
+ continue
567
+ model.register_parameter(name, torch.nn.Parameter(disk_map[name], requires_grad=param.requires_grad))
568
+ for name, buffer in list(model.named_buffers(recurse=False)):
569
+ if buffer is None or name not in disk_map:
570
+ continue
571
+ model.register_buffer(name, disk_map[name], persistent=True)
572
+
573
+
574
+ def enable_vram_management_recursively(model: torch.nn.Module, module_map: dict, vram_config: dict, vram_limit=None, name_prefix="", disk_map=None, quantize=None, **kwargs):
575
+ if isinstance(model, AutoWrappedNonRecurseModule):
576
+ model = model.module
577
+ for name, module in model.named_children():
578
+ layer_name = name if name_prefix == "" else name_prefix + "." + name
579
+ if quantize is not None and quantize.is_quantized_linear(module):
580
+ module_ = AutoWrappedQuantizedModule(module, **vram_config, vram_limit=vram_limit, name=layer_name, disk_map=disk_map, quantize=quantize, **kwargs)
581
+ setattr(model, name, module_)
582
+ continue
583
+ for source_module, target_module in module_map.items():
584
+ if isinstance(module, source_module):
585
+ module_ = target_module(module, **vram_config, vram_limit=vram_limit, name=layer_name, disk_map=disk_map, **kwargs)
586
+ if isinstance(module_, AutoWrappedNonRecurseModule):
587
+ enable_vram_management_recursively(module_, module_map, vram_config, vram_limit=vram_limit, name_prefix=layer_name, disk_map=disk_map, quantize=quantize, **kwargs)
588
+ setattr(model, name, module_)
589
+ break
590
+ else:
591
+ enable_vram_management_recursively(module, module_map, vram_config, vram_limit=vram_limit, name_prefix=layer_name, disk_map=disk_map, quantize=quantize, **kwargs)
592
+
593
+
594
+ def fill_vram_config(model, vram_config):
595
+ vram_config_ = vram_config.copy()
596
+ vram_config_["onload_dtype"] = vram_config["computation_dtype"]
597
+ vram_config_["onload_device"] = vram_config["computation_device"]
598
+ vram_config_["preparing_dtype"] = vram_config["computation_dtype"]
599
+ vram_config_["preparing_device"] = vram_config["computation_device"]
600
+ for k in vram_config:
601
+ if vram_config[k] != vram_config_[k]:
602
+ print(f"No fine-grained VRAM configuration is provided for {model.__class__.__name__}. [`onload`, `preparing`, `computation`] will be the same state. `vram_config` is set to {vram_config_}")
603
+ break
604
+ return vram_config_
605
+
606
+
607
+ def enable_vram_management(model: torch.nn.Module, module_map: dict, vram_config: dict, vram_limit=None, disk_map=None, quantize=None, **kwargs):
608
+ for source_module, target_module in module_map.items():
609
+ # If no fine-grained VRAM configuration is provided, the entire model will be managed uniformly.
610
+ if isinstance(model, source_module):
611
+ vram_config = fill_vram_config(model, vram_config)
612
+ model = target_module(model, **vram_config, vram_limit=vram_limit, disk_map=disk_map, **kwargs)
613
+ break
614
+ else:
615
+ enable_vram_management_recursively(model, module_map, vram_config, vram_limit=vram_limit, disk_map=disk_map, quantize=quantize, **kwargs)
616
+ if disk_map is not None:
617
+ _materialize_root_params_from_disk(model, disk_map)
618
+ # `vram_management_enabled` is a flag that allows the pipeline to determine whether VRAM management is enabled.
619
+ model.vram_management_enabled = True
620
+ return model
diffsynth/diffusion/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from .flow_match import FlowMatchScheduler, HiDreamO1FlashScheduler
2
+ from .training_module import DiffusionTrainingModule
3
+ from .logger import ModelLogger
4
+ from .runner import launch_training_task, launch_data_process_task
5
+ from .parsers import *
6
+ from .loss import *
7
+ from .dmd2 import *
diffsynth/diffusion/base_pipeline.py ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ import torch
3
+ import numpy as np
4
+ from einops import repeat, reduce
5
+ from typing import Union
6
+ from ..core import AutoTorchModule, AutoWrappedLinear, LoRAHotLoadMixin, load_state_dict, ModelConfig, parse_device_type, enable_vram_management
7
+ from ..core.device.npu_compatible_device import get_device_type
8
+ from ..utils.lora import GeneralLoRALoader
9
+ from ..models.model_loader import ModelPool
10
+ from ..utils.controlnet import ControlNetInput
11
+ from ..core.device import get_device_name, IS_NPU_AVAILABLE
12
+ from .template import load_template_model, load_template_data_processor
13
+
14
+
15
+ class PipelineUnit:
16
+ def __init__(
17
+ self,
18
+ seperate_cfg: bool = False,
19
+ take_over: bool = False,
20
+ input_params: tuple[str] = None,
21
+ output_params: tuple[str] = None,
22
+ input_params_posi: dict[str, str] = None,
23
+ input_params_nega: dict[str, str] = None,
24
+ onload_model_names: tuple[str] = None
25
+ ):
26
+ self.seperate_cfg = seperate_cfg
27
+ self.take_over = take_over
28
+ self.input_params = input_params
29
+ self.output_params = output_params
30
+ self.input_params_posi = input_params_posi
31
+ self.input_params_nega = input_params_nega
32
+ self.onload_model_names = onload_model_names
33
+
34
+ def fetch_input_params(self):
35
+ params = []
36
+ if self.input_params is not None:
37
+ for param in self.input_params:
38
+ params.append(param)
39
+ if self.input_params_posi is not None:
40
+ for _, param in self.input_params_posi.items():
41
+ params.append(param)
42
+ if self.input_params_nega is not None:
43
+ for _, param in self.input_params_nega.items():
44
+ params.append(param)
45
+ params = sorted(list(set(params)))
46
+ return params
47
+
48
+ def fetch_output_params(self):
49
+ params = []
50
+ if self.output_params is not None:
51
+ for param in self.output_params:
52
+ params.append(param)
53
+ return params
54
+
55
+ def process(self, pipe, **kwargs) -> dict:
56
+ return {}
57
+
58
+ def post_process(self, pipe, **kwargs) -> dict:
59
+ return {}
60
+
61
+
62
+ class BasePipeline(torch.nn.Module):
63
+
64
+ def __init__(
65
+ self,
66
+ device=get_device_type(), torch_dtype=torch.float16,
67
+ height_division_factor=64, width_division_factor=64,
68
+ time_division_factor=None, time_division_remainder=None,
69
+ ):
70
+ super().__init__()
71
+ # The device and torch_dtype is used for the storage of intermediate variables, not models.
72
+ self.device = device
73
+ self.torch_dtype = torch_dtype
74
+ self.device_type = parse_device_type(device)
75
+ # The following parameters are used for shape check.
76
+ self.height_division_factor = height_division_factor
77
+ self.width_division_factor = width_division_factor
78
+ self.time_division_factor = time_division_factor
79
+ self.time_division_remainder = time_division_remainder
80
+ # VRAM management
81
+ self.vram_management_enabled = False
82
+ # Pipeline Unit Runner
83
+ self.unit_runner = PipelineUnitRunner()
84
+ # LoRA Loader
85
+ self.lora_loader = GeneralLoRALoader
86
+
87
+
88
+ def to(self, *args, **kwargs):
89
+ device, dtype, non_blocking, convert_to_format = torch._C._nn._parse_to(*args, **kwargs)
90
+ if device is not None:
91
+ self.device = device
92
+ if dtype is not None:
93
+ self.torch_dtype = dtype
94
+ super().to(*args, **kwargs)
95
+ return self
96
+
97
+
98
+ def check_resize_height_width(self, height, width, num_frames=None, verbose=1):
99
+ # Shape check
100
+ if height % self.height_division_factor != 0:
101
+ height = (height + self.height_division_factor - 1) // self.height_division_factor * self.height_division_factor
102
+ if verbose > 0:
103
+ print(f"height % {self.height_division_factor} != 0. We round it up to {height}.")
104
+ if width % self.width_division_factor != 0:
105
+ width = (width + self.width_division_factor - 1) // self.width_division_factor * self.width_division_factor
106
+ if verbose > 0:
107
+ print(f"width % {self.width_division_factor} != 0. We round it up to {width}.")
108
+ if num_frames is None:
109
+ return height, width
110
+ else:
111
+ if num_frames % self.time_division_factor != self.time_division_remainder:
112
+ num_frames = (num_frames - self.time_division_remainder + self.time_division_factor - 1) // self.time_division_factor * self.time_division_factor + self.time_division_remainder
113
+ if verbose > 0:
114
+ print(f"num_frames % {self.time_division_factor} != {self.time_division_remainder}. We round it up to {num_frames}.")
115
+ return height, width, num_frames
116
+
117
+
118
+ def preprocess_image(self, image, torch_dtype=None, device=None, pattern="B C H W", min_value=-1, max_value=1):
119
+ # Transform a PIL.Image to torch.Tensor
120
+ image = torch.Tensor(np.array(image, dtype=np.float32))
121
+ image = image.to(dtype=torch_dtype or self.torch_dtype, device=device or self.device)
122
+ image = image * ((max_value - min_value) / 255) + min_value
123
+ image = repeat(image, f"H W C -> {pattern}", **({"B": 1} if "B" in pattern else {}))
124
+ return image
125
+
126
+
127
+ def preprocess_video(self, video, torch_dtype=None, device=None, pattern="B C T H W", min_value=-1, max_value=1):
128
+ # Transform a list of PIL.Image to torch.Tensor
129
+ video = [self.preprocess_image(image, torch_dtype=torch_dtype, device=device, min_value=min_value, max_value=max_value) for image in video]
130
+ video = torch.stack(video, dim=pattern.index("T") // 2)
131
+ return video
132
+
133
+
134
+ def vae_output_to_image(self, vae_output, pattern="B C H W", min_value=-1, max_value=1):
135
+ # Transform a torch.Tensor to PIL.Image
136
+ if pattern != "H W C":
137
+ vae_output = reduce(vae_output, f"{pattern} -> H W C", reduction="mean")
138
+ image = ((vae_output - min_value) * (255 / (max_value - min_value))).clip(0, 255)
139
+ image = image.to(device="cpu", dtype=torch.uint8)
140
+ image = Image.fromarray(image.numpy())
141
+ return image
142
+
143
+
144
+ def vae_output_to_video(self, vae_output, pattern="B C T H W", min_value=-1, max_value=1):
145
+ # Transform a torch.Tensor to list of PIL.Image
146
+ if pattern != "T H W C":
147
+ vae_output = reduce(vae_output, f"{pattern} -> T H W C", reduction="mean")
148
+ video = [self.vae_output_to_image(image, pattern="H W C", min_value=min_value, max_value=max_value) for image in vae_output]
149
+ return video
150
+
151
+ def output_audio_format_check(self, audio_output):
152
+ # output standard foramt: [C, T], output dtype: float()
153
+ # remove batch dim
154
+ if audio_output.ndim == 3:
155
+ audio_output = audio_output.squeeze(0)
156
+ return audio_output.float().cpu()
157
+
158
+ def load_models_to_device(self, model_names):
159
+ if self.vram_management_enabled:
160
+ # offload models
161
+ for name, model in self.named_children():
162
+ if name not in model_names:
163
+ if hasattr(model, "vram_management_enabled") and model.vram_management_enabled:
164
+ if hasattr(model, "offload"):
165
+ model.offload()
166
+ else:
167
+ for module in model.modules():
168
+ if hasattr(module, "offload"):
169
+ module.offload()
170
+ getattr(torch, self.device_type).empty_cache()
171
+ # onload models
172
+ for name, model in self.named_children():
173
+ if name in model_names:
174
+ if hasattr(model, "vram_management_enabled") and model.vram_management_enabled:
175
+ if hasattr(model, "onload"):
176
+ model.onload()
177
+ else:
178
+ for module in model.modules():
179
+ if hasattr(module, "onload"):
180
+ module.onload()
181
+
182
+
183
+ def generate_noise(self, shape, seed=None, rand_device="cpu", rand_torch_dtype=torch.float32, device=None, torch_dtype=None):
184
+ # Initialize Gaussian noise
185
+ generator = None if seed is None else torch.Generator(rand_device).manual_seed(seed)
186
+ noise = torch.randn(shape, generator=generator, device=rand_device, dtype=rand_torch_dtype)
187
+ noise = noise.to(dtype=torch_dtype or self.torch_dtype, device=device or self.device)
188
+ return noise
189
+
190
+
191
+ def get_vram(self):
192
+ device = self.device if not IS_NPU_AVAILABLE else get_device_name()
193
+ return getattr(torch, self.device_type).mem_get_info(device)[1] / (1024 ** 3)
194
+
195
+ def get_module(self, model, name):
196
+ if "." in name:
197
+ name, suffix = name[:name.index(".")], name[name.index(".") + 1:]
198
+ if name.isdigit():
199
+ return self.get_module(model[int(name)], suffix)
200
+ else:
201
+ return self.get_module(getattr(model, name), suffix)
202
+ else:
203
+ return getattr(model, name)
204
+
205
+ def freeze_except(self, model_names):
206
+ self.eval()
207
+ self.requires_grad_(False)
208
+ for name in model_names:
209
+ module = self.get_module(self, name)
210
+ if module is None:
211
+ print(f"No {name} models in the pipeline. We cannot enable training on the model. If this occurs during the data processing stage, it is normal.")
212
+ continue
213
+ module.train()
214
+ module.requires_grad_(True)
215
+
216
+
217
+ def blend_with_mask(self, base, addition, mask):
218
+ return base * (1 - mask) + addition * mask
219
+
220
+
221
+ def step(self, scheduler, latents, progress_id, noise_pred, input_latents=None, inpaint_mask=None, **kwargs):
222
+ timestep = scheduler.timesteps[progress_id]
223
+ if inpaint_mask is not None:
224
+ noise_pred_expected = scheduler.return_to_timestep(scheduler.timesteps[progress_id], latents, input_latents)
225
+ noise_pred = self.blend_with_mask(noise_pred_expected, noise_pred, inpaint_mask)
226
+ latents_next = scheduler.step(noise_pred, timestep, latents)
227
+ return latents_next
228
+
229
+
230
+ def split_pipeline_units(self, model_names: list[str]):
231
+ return PipelineUnitGraph().split_pipeline_units(self.units, model_names)
232
+
233
+
234
+ def flush_vram_management_device(self, device):
235
+ for module in self.modules():
236
+ if isinstance(module, AutoTorchModule):
237
+ module.offload_device = device
238
+ module.onload_device = device
239
+ module.preparing_device = device
240
+ module.computation_device = device
241
+
242
+
243
+ def check_quant_hot_load(self, module: torch.nn.Module):
244
+ # A quantized weight cannot absorb a fused LoRA, so hot-loading is the only option for it.
245
+ if getattr(module, "quantize_config", None) is None:
246
+ return module
247
+ return self.enable_lora_hot_loading(module)
248
+
249
+
250
+ def load_lora(
251
+ self,
252
+ module: torch.nn.Module,
253
+ lora_config: Union[ModelConfig, str] = None,
254
+ alpha=1,
255
+ hotload=None,
256
+ state_dict=None,
257
+ verbose=1,
258
+ ):
259
+ module = self.check_quant_hot_load(module)
260
+ if state_dict is None:
261
+ if isinstance(lora_config, str):
262
+ lora = load_state_dict(lora_config, torch_dtype=self.torch_dtype, device=self.device)
263
+ else:
264
+ lora_config.download_if_necessary()
265
+ lora = load_state_dict(lora_config.path, torch_dtype=self.torch_dtype, device=self.device)
266
+ else:
267
+ lora = state_dict
268
+ lora_loader = self.lora_loader(torch_dtype=self.torch_dtype, device=self.device)
269
+ lora = lora_loader.convert_state_dict(lora)
270
+ if hotload is None:
271
+ hotload = hasattr(module, "vram_management_enabled") and getattr(module, "vram_management_enabled")
272
+ if hotload:
273
+ if not (hasattr(module, "vram_management_enabled") and getattr(module, "vram_management_enabled")):
274
+ raise ValueError("VRAM Management is not enabled. LoRA hotloading is not supported.")
275
+ updated_num = 0
276
+ for _, module in module.named_modules():
277
+ if isinstance(module, LoRAHotLoadMixin):
278
+ name = module.name
279
+ lora_a_name = f'{name}.lora_A.weight'
280
+ lora_b_name = f'{name}.lora_B.weight'
281
+ if lora_a_name in lora and lora_b_name in lora:
282
+ updated_num += 1
283
+ module.lora_A_weights.append(lora[lora_a_name] * alpha)
284
+ module.lora_B_weights.append(lora[lora_b_name])
285
+ if verbose >= 1:
286
+ print(f"{updated_num} tensors are patched by LoRA. You can use `pipe.clear_lora()` to clear all LoRA layers.")
287
+ else:
288
+ lora_loader.fuse_lora_to_base_model(module, lora, alpha=alpha)
289
+
290
+
291
+ def clear_lora(self, verbose=1):
292
+ cleared_num = 0
293
+ for name, module in self.named_modules():
294
+ if isinstance(module, LoRAHotLoadMixin):
295
+ if hasattr(module, "lora_A_weights"):
296
+ if len(module.lora_A_weights) > 0:
297
+ cleared_num += 1
298
+ module.lora_A_weights.clear()
299
+ if hasattr(module, "lora_B_weights"):
300
+ module.lora_B_weights.clear()
301
+ if verbose >= 1:
302
+ print(f"{cleared_num} LoRA layers are cleared.")
303
+
304
+
305
+ def download_and_load_models(self, model_configs: list[ModelConfig] = [], vram_limit: float = None):
306
+ model_pool = ModelPool()
307
+ for model_config in model_configs:
308
+ model_config.download_if_necessary()
309
+ vram_config = model_config.vram_config()
310
+ vram_config["computation_dtype"] = vram_config["computation_dtype"] or self.torch_dtype
311
+ vram_config["computation_device"] = vram_config["computation_device"] or self.device
312
+ model_pool.auto_load_model(
313
+ model_config.path,
314
+ vram_config=vram_config,
315
+ vram_limit=vram_limit,
316
+ clear_parameters=model_config.clear_parameters,
317
+ state_dict=model_config.state_dict,
318
+ quantize=model_config.quantize,
319
+ )
320
+ return model_pool
321
+
322
+
323
+ def check_vram_management_state(self):
324
+ vram_management_enabled = False
325
+ for module in self.children():
326
+ if hasattr(module, "vram_management_enabled") and getattr(module, "vram_management_enabled"):
327
+ vram_management_enabled = True
328
+ return vram_management_enabled
329
+
330
+
331
+ def cfg_guided_model_fn(self, model_fn, cfg_scale, inputs_shared, inputs_posi, inputs_nega, **inputs_others):
332
+ # Positive side forward
333
+ if inputs_shared.get("positive_only_lora", None) is not None:
334
+ self.load_lora(self.dit, state_dict=inputs_shared["positive_only_lora"], verbose=0)
335
+ noise_pred_posi = model_fn(**inputs_posi, **inputs_shared, **inputs_others)
336
+ if inputs_shared.get("positive_only_lora", None) is not None:
337
+ self.clear_lora(verbose=0)
338
+
339
+ if cfg_scale != 1.0:
340
+ # Negative side forward
341
+ if inputs_shared.get("negative_only_lora", None) is not None:
342
+ self.load_lora(self.dit, state_dict=inputs_shared["negative_only_lora"], verbose=0)
343
+ noise_pred_nega = model_fn(**inputs_nega, **inputs_shared, **inputs_others)
344
+ if inputs_shared.get("negative_only_lora", None) is not None:
345
+ self.clear_lora(verbose=0)
346
+
347
+ if isinstance(noise_pred_posi, tuple):
348
+ # Separately handling different output types of latents, eg. video and audio latents.
349
+ noise_pred = tuple(
350
+ n_nega + cfg_scale * (n_posi - n_nega)
351
+ for n_posi, n_nega in zip(noise_pred_posi, noise_pred_nega)
352
+ )
353
+ else:
354
+ noise_pred = noise_pred_nega + cfg_scale * (noise_pred_posi - noise_pred_nega)
355
+ else:
356
+ noise_pred = noise_pred_posi
357
+ return noise_pred
358
+
359
+
360
+ def load_training_template_model(self, model_config: ModelConfig = None):
361
+ if model_config is not None:
362
+ model_config.download_if_necessary()
363
+ self.template_model = load_template_model(model_config.path, torch_dtype=self.torch_dtype, device=self.device)
364
+ self.template_data_processor = load_template_data_processor(model_config.path)()
365
+
366
+
367
+ def enable_lora_hot_loading(self, model: torch.nn.Module):
368
+ if hasattr(model, "vram_management_enabled") and getattr(model, "vram_management_enabled"):
369
+ return model
370
+ module_map = {torch.nn.Linear: AutoWrappedLinear}
371
+ vram_config = {
372
+ "offload_dtype": self.torch_dtype,
373
+ "offload_device": self.device,
374
+ "onload_dtype": self.torch_dtype,
375
+ "onload_device": self.device,
376
+ "preparing_dtype": self.torch_dtype,
377
+ "preparing_device": self.device,
378
+ "computation_dtype": self.torch_dtype,
379
+ "computation_device": self.device,
380
+ }
381
+ model = enable_vram_management(model, module_map, vram_config=vram_config, quantize=getattr(model, "quantize_config", None))
382
+ return model
383
+
384
+ def compile_pipeline(self, mode: str = "default", dynamic: bool = True, fullgraph: bool = False, compile_models: list = None, **kwargs):
385
+ """
386
+ compile the pipeline with torch.compile. The models that will be compiled are determined by the `compilable_models` attribute of the pipeline.
387
+ If a model has `_repeated_blocks` attribute, we will compile these blocks with regional compilation. Otherwise, we will compile the whole model.
388
+ See https://docs.pytorch.org/docs/stable/generated/torch.compile.html#torch.compile for details about compilation arguments.
389
+ Args:
390
+ mode: The compilation mode, which will be passed to `torch.compile`, options are "default", "reduce-overhead", "max-autotune" and "max-autotune-no-cudagraphs. Default to "default".
391
+ dynamic: Whether to enable dynamic graph compilation to support dynamic input shapes, which will be passed to `torch.compile`. Default to True (recommended).
392
+ fullgraph: Whether to use full graph compilation, which will be passed to `torch.compile`. Default to False (recommended).
393
+ compile_models: The list of model names to be compiled. If None, we will compile the models in `pipeline.compilable_models`. Default to None.
394
+ **kwargs: Other arguments for `torch.compile`.
395
+ """
396
+ compile_models = compile_models or getattr(self, "compilable_models", [])
397
+ if len(compile_models) == 0:
398
+ print("No compilable models in the pipeline. Skip compilation.")
399
+ return
400
+ for name in compile_models:
401
+ model = getattr(self, name, None)
402
+ if model is None:
403
+ print(f"Model '{name}' not found in the pipeline.")
404
+ continue
405
+ repeated_blocks = getattr(model, "_repeated_blocks", None)
406
+ # regional compilation for repeated blocks.
407
+ if repeated_blocks is not None:
408
+ for submod in model.modules():
409
+ if submod.__class__.__name__ in repeated_blocks:
410
+ submod.compile(mode=mode, dynamic=dynamic, fullgraph=fullgraph, **kwargs)
411
+ # compile the whole model.
412
+ else:
413
+ model.compile(mode=mode, dynamic=dynamic, fullgraph=fullgraph, **kwargs)
414
+ print(f"{name} is compiled with mode={mode}, dynamic={dynamic}, fullgraph={fullgraph}.")
415
+
416
+
417
+ class PipelineUnitGraph:
418
+ def __init__(self):
419
+ pass
420
+
421
+ def build_edges(self, units: list[PipelineUnit]):
422
+ # Establish dependencies between units
423
+ # to search for subsequent related computation units.
424
+ last_compute_unit_id = {}
425
+ edges = []
426
+ for unit_id, unit in enumerate(units):
427
+ for input_param in unit.fetch_input_params():
428
+ if input_param in last_compute_unit_id:
429
+ edges.append((last_compute_unit_id[input_param], unit_id))
430
+ for output_param in unit.fetch_output_params():
431
+ last_compute_unit_id[output_param] = unit_id
432
+ return edges
433
+
434
+ def build_chains(self, units: list[PipelineUnit]):
435
+ # Establish updating chains for each variable
436
+ # to track their computation process.
437
+ params = sum([unit.fetch_input_params() + unit.fetch_output_params() for unit in units], [])
438
+ params = sorted(list(set(params)))
439
+ chains = {param: [] for param in params}
440
+ for unit_id, unit in enumerate(units):
441
+ for param in unit.fetch_output_params():
442
+ chains[param].append(unit_id)
443
+ return chains
444
+
445
+ def search_direct_unit_ids(self, units: list[PipelineUnit], model_names: list[str]):
446
+ # Search for units that directly participate in the model's computation.
447
+ related_unit_ids = []
448
+ for unit_id, unit in enumerate(units):
449
+ for model_name in model_names:
450
+ if unit.onload_model_names is not None and model_name in unit.onload_model_names:
451
+ related_unit_ids.append(unit_id)
452
+ break
453
+ return related_unit_ids
454
+
455
+ def search_related_unit_ids(self, edges, start_unit_ids, direction="target"):
456
+ # Search for subsequent related computation units.
457
+ related_unit_ids = [unit_id for unit_id in start_unit_ids]
458
+ while True:
459
+ neighbors = []
460
+ for source, target in edges:
461
+ if direction == "target" and source in related_unit_ids and target not in related_unit_ids:
462
+ neighbors.append(target)
463
+ elif direction == "source" and source not in related_unit_ids and target in related_unit_ids:
464
+ neighbors.append(source)
465
+ neighbors = sorted(list(set(neighbors)))
466
+ if len(neighbors) == 0:
467
+ break
468
+ else:
469
+ related_unit_ids.extend(neighbors)
470
+ related_unit_ids = sorted(list(set(related_unit_ids)))
471
+ return related_unit_ids
472
+
473
+ def search_updating_unit_ids(self, units: list[PipelineUnit], chains, related_unit_ids):
474
+ # If the input parameters of this subgraph are updated outside the subgraph,
475
+ # search for the units where these updates occur.
476
+ first_compute_unit_id = {}
477
+ for unit_id in related_unit_ids:
478
+ for param in units[unit_id].fetch_input_params():
479
+ if param not in first_compute_unit_id:
480
+ first_compute_unit_id[param] = unit_id
481
+ updating_unit_ids = []
482
+ for param in first_compute_unit_id:
483
+ unit_id = first_compute_unit_id[param]
484
+ chain = chains[param]
485
+ if unit_id in chain and chain.index(unit_id) != len(chain) - 1:
486
+ for unit_id_ in chain[chain.index(unit_id) + 1:]:
487
+ if unit_id_ not in related_unit_ids:
488
+ updating_unit_ids.append(unit_id_)
489
+ related_unit_ids.extend(updating_unit_ids)
490
+ related_unit_ids = sorted(list(set(related_unit_ids)))
491
+ return related_unit_ids
492
+
493
+ def split_pipeline_units(self, units: list[PipelineUnit], model_names: list[str]):
494
+ # Split the computation graph,
495
+ # separating all model-related computations.
496
+ related_unit_ids = self.search_direct_unit_ids(units, model_names)
497
+ edges = self.build_edges(units)
498
+ chains = self.build_chains(units)
499
+ while True:
500
+ num_related_unit_ids = len(related_unit_ids)
501
+ related_unit_ids = self.search_related_unit_ids(edges, related_unit_ids, "target")
502
+ related_unit_ids = self.search_updating_unit_ids(units, chains, related_unit_ids)
503
+ if len(related_unit_ids) == num_related_unit_ids:
504
+ break
505
+ else:
506
+ num_related_unit_ids = len(related_unit_ids)
507
+ related_units = [units[i] for i in related_unit_ids]
508
+ unrelated_units = [units[i] for i in range(len(units)) if i not in related_unit_ids]
509
+ return related_units, unrelated_units
510
+
511
+
512
+ class PipelineUnitRunner:
513
+ def __init__(self):
514
+ pass
515
+
516
+ def __call__(self, unit: PipelineUnit, pipe: BasePipeline, inputs_shared: dict, inputs_posi: dict, inputs_nega: dict) -> tuple[dict, dict]:
517
+ if unit.take_over:
518
+ # Let the pipeline unit take over this function.
519
+ inputs_shared, inputs_posi, inputs_nega = unit.process(pipe, inputs_shared=inputs_shared, inputs_posi=inputs_posi, inputs_nega=inputs_nega)
520
+ elif unit.seperate_cfg:
521
+ # Positive side
522
+ processor_inputs = {name: inputs_posi.get(name_) for name, name_ in unit.input_params_posi.items()}
523
+ if unit.input_params is not None:
524
+ for name in unit.input_params:
525
+ processor_inputs[name] = inputs_shared.get(name)
526
+ processor_outputs = unit.process(pipe, **processor_inputs)
527
+ inputs_posi.update(processor_outputs)
528
+ # Negative side
529
+ if inputs_shared["cfg_scale"] != 1:
530
+ processor_inputs = {name: inputs_nega.get(name_) for name, name_ in unit.input_params_nega.items()}
531
+ if unit.input_params is not None:
532
+ for name in unit.input_params:
533
+ processor_inputs[name] = inputs_shared.get(name)
534
+ processor_outputs = unit.process(pipe, **processor_inputs)
535
+ inputs_nega.update(processor_outputs)
536
+ else:
537
+ inputs_nega.update(processor_outputs)
538
+ else:
539
+ processor_inputs = {name: inputs_shared.get(name) for name in unit.input_params}
540
+ processor_outputs = unit.process(pipe, **processor_inputs)
541
+ inputs_shared.update(processor_outputs)
542
+ return inputs_shared, inputs_posi, inputs_nega
diffsynth/diffusion/ddim_scheduler.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch, math
2
+
3
+
4
+ class DDIMScheduler():
5
+
6
+ def __init__(self, num_train_timesteps=1000, beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", prediction_type="epsilon", rescale_zero_terminal_snr=False):
7
+ self.num_train_timesteps = num_train_timesteps
8
+ if beta_schedule == "scaled_linear":
9
+ betas = torch.square(torch.linspace(math.sqrt(beta_start), math.sqrt(beta_end), num_train_timesteps, dtype=torch.float32))
10
+ elif beta_schedule == "linear":
11
+ betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32)
12
+ else:
13
+ raise NotImplementedError(f"{beta_schedule} is not implemented")
14
+ self.alphas_cumprod = torch.cumprod(1.0 - betas, dim=0)
15
+ if rescale_zero_terminal_snr:
16
+ self.alphas_cumprod = self.rescale_zero_terminal_snr(self.alphas_cumprod)
17
+ self.alphas_cumprod = self.alphas_cumprod.tolist()
18
+ self.set_timesteps(10)
19
+ self.prediction_type = prediction_type
20
+ self.training = False
21
+
22
+
23
+ def rescale_zero_terminal_snr(self, alphas_cumprod):
24
+ alphas_bar_sqrt = alphas_cumprod.sqrt()
25
+
26
+ # Store old values.
27
+ alphas_bar_sqrt_0 = alphas_bar_sqrt[0].clone()
28
+ alphas_bar_sqrt_T = alphas_bar_sqrt[-1].clone()
29
+
30
+ # Shift so the last timestep is zero.
31
+ alphas_bar_sqrt -= alphas_bar_sqrt_T
32
+
33
+ # Scale so the first timestep is back to the old value.
34
+ alphas_bar_sqrt *= alphas_bar_sqrt_0 / (alphas_bar_sqrt_0 - alphas_bar_sqrt_T)
35
+
36
+ # Convert alphas_bar_sqrt to betas
37
+ alphas_bar = alphas_bar_sqrt.square() # Revert sqrt
38
+
39
+ return alphas_bar
40
+
41
+
42
+ def set_timesteps(self, num_inference_steps, denoising_strength=1.0, training=False, **kwargs):
43
+ # The timesteps are aligned to 999...0, which is different from other implementations,
44
+ # but I think this implementation is more reasonable in theory.
45
+ max_timestep = max(round(self.num_train_timesteps * denoising_strength) - 1, 0)
46
+ num_inference_steps = min(num_inference_steps, max_timestep + 1)
47
+ if num_inference_steps == 1:
48
+ self.timesteps = torch.Tensor([max_timestep])
49
+ else:
50
+ step_length = max_timestep / (num_inference_steps - 1)
51
+ self.timesteps = torch.Tensor([round(max_timestep - i*step_length) for i in range(num_inference_steps)])
52
+ self.training = training
53
+
54
+
55
+ def denoise(self, model_output, sample, alpha_prod_t, alpha_prod_t_prev):
56
+ if self.prediction_type == "epsilon":
57
+ weight_e = math.sqrt(1 - alpha_prod_t_prev) - math.sqrt(alpha_prod_t_prev * (1 - alpha_prod_t) / alpha_prod_t)
58
+ weight_x = math.sqrt(alpha_prod_t_prev / alpha_prod_t)
59
+ prev_sample = sample * weight_x + model_output * weight_e
60
+ elif self.prediction_type == "v_prediction":
61
+ weight_e = -math.sqrt(alpha_prod_t_prev * (1 - alpha_prod_t)) + math.sqrt(alpha_prod_t * (1 - alpha_prod_t_prev))
62
+ weight_x = math.sqrt(alpha_prod_t * alpha_prod_t_prev) + math.sqrt((1 - alpha_prod_t) * (1 - alpha_prod_t_prev))
63
+ prev_sample = sample * weight_x + model_output * weight_e
64
+ else:
65
+ raise NotImplementedError(f"{self.prediction_type} is not implemented")
66
+ return prev_sample
67
+
68
+
69
+ def step(self, model_output, timestep, sample, to_final=False):
70
+ alpha_prod_t = self.alphas_cumprod[int(timestep.flatten().tolist()[0])]
71
+ if isinstance(timestep, torch.Tensor):
72
+ timestep = timestep.cpu()
73
+ timestep_id = torch.argmin((self.timesteps - timestep).abs())
74
+ if to_final or timestep_id + 1 >= len(self.timesteps):
75
+ alpha_prod_t_prev = 1.0
76
+ else:
77
+ timestep_prev = int(self.timesteps[timestep_id + 1])
78
+ alpha_prod_t_prev = self.alphas_cumprod[timestep_prev]
79
+
80
+ return self.denoise(model_output, sample, alpha_prod_t, alpha_prod_t_prev)
81
+
82
+
83
+ def return_to_timestep(self, timestep, sample, sample_stablized):
84
+ alpha_prod_t = self.alphas_cumprod[int(timestep.flatten().tolist()[0])]
85
+ noise_pred = (sample - math.sqrt(alpha_prod_t) * sample_stablized) / math.sqrt(1 - alpha_prod_t)
86
+ return noise_pred
87
+
88
+
89
+ def add_noise(self, original_samples, noise, timestep):
90
+ timestep_id = max(min(int(timestep.flatten().tolist()[0]), len(self.alphas_cumprod)-1), 0)
91
+ sqrt_alpha_prod = math.sqrt(self.alphas_cumprod[timestep_id])
92
+ sqrt_one_minus_alpha_prod = math.sqrt(1 - self.alphas_cumprod[timestep_id])
93
+ noisy_samples = sqrt_alpha_prod * original_samples + sqrt_one_minus_alpha_prod * noise
94
+ return noisy_samples
95
+
96
+
97
+ def training_target(self, sample, noise, timestep):
98
+ if self.prediction_type == "epsilon":
99
+ return noise
100
+ else:
101
+ timestep_id = max(min(int(timestep.flatten().tolist()[0]), len(self.alphas_cumprod)-1), 0)
102
+ sqrt_alpha_prod = math.sqrt(self.alphas_cumprod[timestep_id])
103
+ sqrt_one_minus_alpha_prod = math.sqrt(1 - self.alphas_cumprod[timestep_id])
104
+ target = sqrt_alpha_prod * noise - sqrt_one_minus_alpha_prod * sample
105
+ return target
106
+
107
+
108
+ def training_weight(self, timestep):
109
+ return 1.0
diffsynth/diffusion/dmd2.py ADDED
@@ -0,0 +1,601 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+ import torch
4
+ import torch.nn.functional as F
5
+ from accelerate import Accelerator
6
+ from tqdm import tqdm
7
+ from .runner import get_optimizer_class, initialize_deepspeed_gradient_checkpointing
8
+ from diffsynth.core import OffloadTrainingManager
9
+
10
+
11
+ @dataclass
12
+ class DMD2Config:
13
+ student_update_freq: int = 5
14
+ student_sample_steps: int = 4
15
+ student_sample_type: str = "sde"
16
+ student_schedule: str = "uniform"
17
+ student_t_list: Optional[list[float]] = None
18
+ matching_t_min: float = 0.001
19
+ matching_t_max: float = 0.999
20
+ matching_t_sampling: str = "uniform"
21
+ matching_t_mean: float = 0.0
22
+ matching_t_std: float = 1.0
23
+ gan_loss_weight: float = 0.03
24
+ gan_r1_reg_weight: float = 0.0
25
+ gan_r1_reg_alpha: float = 0.1
26
+ fake_score_learning_rate: Optional[float] = None
27
+ discriminator_learning_rate: Optional[float] = None
28
+ feature_indices: Optional[list[int]] = None
29
+ teacher_cfg_scale: float = 1.0
30
+ student_grad_clip_norm: Optional[float] = 10.0
31
+
32
+
33
+ def _get_dmd2_pipe_model(module, model_name):
34
+ if model_name is None:
35
+ return None
36
+ return getattr(module.pipe, model_name)
37
+
38
+
39
+ def get_dmd2_student_model(module):
40
+ return _get_dmd2_pipe_model(module, module.dmd2_student_model_name)
41
+
42
+
43
+ def get_dmd2_teacher_model(module):
44
+ return _get_dmd2_pipe_model(module, module.dmd2_teacher_model_name)
45
+
46
+
47
+ def get_dmd2_fake_score_model(module):
48
+ return _get_dmd2_pipe_model(module, module.dmd2_fake_score_model_name)
49
+
50
+
51
+ def get_dmd2_discriminator(module):
52
+ return _get_dmd2_pipe_model(module, module.dmd2_discriminator_model_name)
53
+
54
+
55
+ def _dmd2_model_state_names(module, model_name, param_names=None):
56
+ if model_name is None:
57
+ return set()
58
+ model = getattr(module.pipe, model_name, None)
59
+ if model is None:
60
+ return set()
61
+ names = model.state_dict().keys() if param_names is None else param_names
62
+ return {f"pipe.{model_name}.{name}" for name in names}
63
+
64
+
65
+ def export_dmd2_trainable_state_dict(module, state_dict, remove_prefix=None):
66
+ student_names = _dmd2_model_state_names(module, module.dmd2_student_model_name, module._dmd2_student_param_names)
67
+ state_names = set(student_names)
68
+ if remove_prefix is None:
69
+ state_names.update(_dmd2_model_state_names(module, module.dmd2_teacher_model_name))
70
+ state_names.update(_dmd2_model_state_names(module, module.dmd2_fake_score_model_name))
71
+ state_names.update(_dmd2_model_state_names(module, module.dmd2_discriminator_model_name))
72
+ state_dict = {name: param for name, param in state_dict.items() if name in state_names}
73
+ if remove_prefix is not None:
74
+ state_dict = {name[len(remove_prefix):] if name.startswith(remove_prefix) else name: param for name, param in state_dict.items()}
75
+ return state_dict
76
+
77
+
78
+ def set_dmd2_train_phase(module, student_phase: bool):
79
+ student_model = get_dmd2_student_model(module)
80
+ student_model.train(student_phase)
81
+ for name, param in student_model.named_parameters():
82
+ param.requires_grad = student_phase and name in module._dmd2_student_param_names
83
+
84
+ fake_score_model = get_dmd2_fake_score_model(module)
85
+ fake_score_model.train(not student_phase)
86
+ for name, param in fake_score_model.named_parameters():
87
+ param.requires_grad = (not student_phase) and name in module._dmd2_fake_score_param_names
88
+
89
+ discriminator = get_dmd2_discriminator(module)
90
+ if discriminator is not None:
91
+ discriminator.train(not student_phase)
92
+ discriminator.requires_grad_(not student_phase)
93
+
94
+
95
+ def _expand_like(value, target, dtype=None):
96
+ if dtype is None:
97
+ dtype = target.dtype
98
+ if not isinstance(value, torch.Tensor):
99
+ value = torch.tensor(value, device=target.device, dtype=dtype)
100
+ value = value.to(device=target.device, dtype=dtype)
101
+ while value.ndim < target.ndim:
102
+ value = value.view(*value.shape, 1)
103
+ return value
104
+
105
+
106
+ def flow_to_x0(latents, flow, sigma):
107
+ original_dtype = latents.dtype
108
+ latents = latents.to(torch.float64)
109
+ flow = flow.to(torch.float64)
110
+ sigma = _expand_like(sigma, latents)
111
+ return (latents - sigma * flow).to(original_dtype)
112
+
113
+
114
+ def _forward_process(x0, eps, sigma):
115
+ original_dtype = x0.dtype
116
+ x0 = x0.to(torch.float64)
117
+ eps = eps.to(torch.float64)
118
+ sigma = _expand_like(sigma, x0)
119
+ return ((1 - sigma) * x0 + sigma * eps).to(original_dtype)
120
+
121
+
122
+ def _scale_noise(noise, sigma):
123
+ original_dtype = noise.dtype
124
+ noise = noise.to(torch.float64)
125
+ sigma = _expand_like(sigma, noise)
126
+ return (noise * sigma).to(original_dtype)
127
+
128
+
129
+ def make_dmd2_student_schedule(
130
+ pipe,
131
+ num_steps,
132
+ device,
133
+ student_schedule="uniform",
134
+ student_t_list=None,
135
+ matching_t_max=0.999,
136
+ ):
137
+ time_dtype = torch.float64
138
+ if student_t_list is not None:
139
+ sigmas = torch.tensor(student_t_list, device=device, dtype=time_dtype)
140
+ if sigmas[-1].item() != 0:
141
+ raise ValueError("`dmd2_student_t_list` must include a final 0.")
142
+ if len(sigmas) != num_steps + 1:
143
+ raise ValueError("The student sigma schedule length must equal `student_sample_steps + 1`.")
144
+ timesteps = sigmas * pipe.scheduler.num_train_timesteps
145
+ return sigmas, timesteps
146
+ if student_schedule == "uniform":
147
+ sigma_start = min(float(matching_t_max), 0.999)
148
+ sigmas = torch.linspace(sigma_start, 0.0, num_steps + 1, device=device, dtype=time_dtype)
149
+ timesteps = sigmas * pipe.scheduler.num_train_timesteps
150
+ return sigmas, timesteps
151
+ raise ValueError(f"Unsupported DMD2 student schedule: {student_schedule}")
152
+
153
+
154
+ def _variational_score_distillation_loss(gen_data, teacher_x0, fake_score_x0):
155
+ dims = tuple(range(1, teacher_x0.ndim))
156
+ with torch.no_grad():
157
+ weight = 1 / ((gen_data.float() - teacher_x0.float()).abs().mean(dim=dims, keepdim=True) + 1e-6)
158
+ weight = weight.to(dtype=gen_data.dtype)
159
+ pseudo_target = gen_data - (fake_score_x0 - teacher_x0) * weight
160
+ loss = 0.5 * F.mse_loss(gen_data.float(), pseudo_target.float(), reduction="mean")
161
+ return loss
162
+
163
+
164
+ def _gan_loss_generator(fake_logits):
165
+ assert fake_logits.ndim == 2, f"fake_logits has shape {fake_logits.shape}"
166
+ gan_loss = F.softplus(-fake_logits).mean()
167
+ return gan_loss
168
+
169
+
170
+ def _gan_loss_discriminator(real_logits, fake_logits):
171
+ assert fake_logits.ndim == 2, f"fake_logits has shape {fake_logits.shape}"
172
+ assert real_logits.ndim == 2, f"real_logits has shape {real_logits.shape}"
173
+ gan_loss = F.softplus(fake_logits).mean() + F.softplus(-real_logits).mean()
174
+ return gan_loss
175
+
176
+
177
+ class DMD2Loss:
178
+ def __init__(self, config: DMD2Config):
179
+ self.config = config
180
+
181
+ def _model_forward_x0(
182
+ self,
183
+ module,
184
+ model,
185
+ model_fn,
186
+ latents,
187
+ timestep,
188
+ sigma,
189
+ inputs_shared,
190
+ inputs_posi,
191
+ ):
192
+ shared = dict(inputs_shared)
193
+ posi = dict(inputs_posi)
194
+ shared["latents"] = latents
195
+ flow = model_fn(
196
+ module.pipe,
197
+ model,
198
+ timestep=timestep,
199
+ progress_id=0,
200
+ num_inference_steps=1,
201
+ inputs_shared=shared,
202
+ inputs_posi=posi,
203
+ )
204
+ return flow_to_x0(latents, flow, sigma)
205
+
206
+ def _model_forward_features(
207
+ self,
208
+ module,
209
+ model,
210
+ model_fn,
211
+ latents,
212
+ timestep,
213
+ inputs_shared,
214
+ inputs_posi,
215
+ ):
216
+ discriminator = get_dmd2_discriminator(module)
217
+ if discriminator is None:
218
+ raise ValueError("DMD2 feature extraction requires a discriminator.")
219
+ if self.config.feature_indices is None:
220
+ raise ValueError("DMD2 feature extraction requires `dmd2_feature_indices`.")
221
+ shared = dict(inputs_shared)
222
+ posi = dict(inputs_posi)
223
+ shared["latents"] = latents
224
+ return model_fn(
225
+ module.pipe,
226
+ model,
227
+ timestep=timestep,
228
+ progress_id=0,
229
+ num_inference_steps=1,
230
+ inputs_shared=shared,
231
+ inputs_posi=posi,
232
+ feature_indices=set(self.config.feature_indices),
233
+ return_features=True,
234
+ )
235
+
236
+ def _teacher_forward_x0(
237
+ self,
238
+ module,
239
+ latents,
240
+ timestep,
241
+ sigma,
242
+ inputs_shared,
243
+ inputs_posi,
244
+ inputs_nega,
245
+ ):
246
+ teacher_x0_pos = self._model_forward_x0(
247
+ module,
248
+ get_dmd2_teacher_model(module),
249
+ module.dmd2_model_fn_teacher,
250
+ latents,
251
+ timestep,
252
+ sigma,
253
+ inputs_shared,
254
+ inputs_posi,
255
+ )
256
+ if self.config.teacher_cfg_scale <= 1.0:
257
+ return teacher_x0_pos
258
+
259
+
260
+ x0_neg = self._model_forward_x0(
261
+ module,
262
+ get_dmd2_teacher_model(module),
263
+ module.dmd2_model_fn_teacher,
264
+ latents,
265
+ timestep,
266
+ sigma,
267
+ inputs_shared,
268
+ inputs_nega,
269
+ )
270
+ return x0_neg + float(self.config.teacher_cfg_scale) * (teacher_x0_pos - x0_neg)
271
+
272
+ def _sample_matching_timestep(self, pipe, device, dtype, batch_size=1, inputs_shared=None, real_data=None):
273
+ if self.config.matching_t_min > self.config.matching_t_max:
274
+ raise ValueError("`dmd2_matching_t_min` must be <= `dmd2_matching_t_max`.")
275
+ time_dtype = torch.float64
276
+ if self.config.matching_t_sampling == "uniform":
277
+ sigma = torch.rand(batch_size, device=device, dtype=time_dtype)
278
+ sigma = sigma * (self.config.matching_t_max - self.config.matching_t_min) + self.config.matching_t_min
279
+ timestep = sigma * pipe.scheduler.num_train_timesteps
280
+ return timestep, sigma
281
+ if self.config.matching_t_sampling == "logitnormal":
282
+ sigma = torch.sigmoid(
283
+ torch.randn(batch_size, device=device, dtype=time_dtype) * self.config.matching_t_std
284
+ + self.config.matching_t_mean
285
+ )
286
+ sigma = sigma * (self.config.matching_t_max - self.config.matching_t_min) + self.config.matching_t_min
287
+ sigma = sigma.clamp(self.config.matching_t_min, self.config.matching_t_max)
288
+ timestep = sigma * pipe.scheduler.num_train_timesteps
289
+ return timestep, sigma
290
+ raise ValueError(f"Unsupported DMD2 matching timestep sampling: {self.config.matching_t_sampling}")
291
+
292
+ def _generate_student_data(self, module, real_data, inputs_shared, inputs_posi):
293
+ pipe = module.pipe
294
+ device, dtype = real_data.device, real_data.dtype
295
+ batch_size = real_data.shape[0]
296
+ student_sigmas, student_timesteps = make_dmd2_student_schedule(
297
+ pipe,
298
+ self.config.student_sample_steps,
299
+ device,
300
+ student_schedule=self.config.student_schedule,
301
+ student_t_list=self.config.student_t_list,
302
+ matching_t_max=self.config.matching_t_max,
303
+ )
304
+ if len(student_sigmas) != self.config.student_sample_steps + 1:
305
+ raise ValueError("The student sigma schedule length must equal `student_sample_steps + 1`.")
306
+
307
+ if self.config.student_sample_steps == 1:
308
+ timestep = student_timesteps[0:1].expand(batch_size)
309
+ sigma = student_sigmas[0:1].expand(batch_size)
310
+ input_student = _scale_noise(torch.randn_like(real_data), sigma)
311
+ else:
312
+ step_id = torch.randint(0, self.config.student_sample_steps, (batch_size,), device=device)
313
+ sigma = student_sigmas[step_id]
314
+ timestep = student_timesteps[step_id]
315
+ eps_student = torch.randn_like(real_data)
316
+ input_student = _forward_process(real_data, eps_student, sigma)
317
+ gen_data = self._model_forward_x0(
318
+ module,
319
+ get_dmd2_student_model(module),
320
+ module.dmd2_model_fn_student,
321
+ input_student,
322
+ timestep,
323
+ sigma,
324
+ inputs_shared,
325
+ inputs_posi,
326
+ )
327
+ return gen_data, input_student
328
+
329
+ def _compute_real_feat(self, module, real_data, timestep, sigma, eps, inputs_shared, inputs_posi):
330
+ real_timestep, real_sigma, real_eps = timestep, sigma, eps
331
+ perturbed_real = _forward_process(real_data, real_eps, real_sigma)
332
+ real_feat = self._model_forward_features(
333
+ module,
334
+ get_dmd2_teacher_model(module),
335
+ module.dmd2_model_fn_teacher,
336
+ perturbed_real,
337
+ real_timestep,
338
+ inputs_shared,
339
+ inputs_posi,
340
+ )
341
+ return real_feat, real_timestep, real_sigma
342
+
343
+ def _student_update_step(self, module, real_data, inputs_shared, inputs_posi, inputs_nega):
344
+ gen_data, input_student = self._generate_student_data(module, real_data, inputs_shared, inputs_posi)
345
+ timestep, sigma = self._sample_matching_timestep(
346
+ module.pipe,
347
+ real_data.device,
348
+ real_data.dtype,
349
+ real_data.shape[0],
350
+ inputs_shared=inputs_shared,
351
+ real_data=real_data,
352
+ )
353
+ eps = torch.randn_like(real_data)
354
+ perturbed_data = _forward_process(gen_data, eps, sigma)
355
+
356
+ with torch.no_grad():
357
+ fake_score_x0 = self._model_forward_x0(
358
+ module,
359
+ get_dmd2_fake_score_model(module),
360
+ module.dmd2_model_fn_fake_score,
361
+ perturbed_data,
362
+ timestep,
363
+ sigma,
364
+ inputs_shared,
365
+ inputs_posi,
366
+ )
367
+
368
+ with torch.no_grad():
369
+ teacher_x0 = self._teacher_forward_x0(
370
+ module, perturbed_data, timestep, sigma, inputs_shared, inputs_posi, inputs_nega
371
+ )
372
+ if self.config.gan_loss_weight > 0:
373
+ fake_feat = self._model_forward_features(
374
+ module,
375
+ get_dmd2_teacher_model(module),
376
+ module.dmd2_model_fn_teacher,
377
+ perturbed_data,
378
+ timestep,
379
+ inputs_shared,
380
+ inputs_posi,
381
+ )
382
+ fake_logits_gen = get_dmd2_discriminator(module)(fake_feat)
383
+ gan_loss_gen = _gan_loss_generator(fake_logits_gen)
384
+ else:
385
+ gan_loss_gen = torch.zeros((), device=real_data.device, dtype=torch.float32)
386
+
387
+ vsd_loss = _variational_score_distillation_loss(gen_data, teacher_x0.detach(), fake_score_x0)
388
+ gan_loss_weighted = self.config.gan_loss_weight * gan_loss_gen
389
+ loss = vsd_loss + gan_loss_weighted
390
+
391
+ return {"total_loss": loss}
392
+
393
+ def _fake_score_discriminator_update_step(self, module, real_data, inputs_shared, inputs_posi, inputs_nega):
394
+ with torch.no_grad():
395
+ gen_data, _ = self._generate_student_data(module, real_data, inputs_shared, inputs_posi)
396
+ timestep, sigma = self._sample_matching_timestep(
397
+ module.pipe,
398
+ real_data.device,
399
+ real_data.dtype,
400
+ real_data.shape[0],
401
+ inputs_shared=inputs_shared,
402
+ real_data=real_data,
403
+ )
404
+ eps = torch.randn_like(real_data)
405
+ x_t_sg = _forward_process(gen_data, eps, sigma)
406
+
407
+ fake_score_x0 = self._model_forward_x0(
408
+ module,
409
+ get_dmd2_fake_score_model(module),
410
+ module.dmd2_model_fn_fake_score,
411
+ x_t_sg,
412
+ timestep,
413
+ sigma,
414
+ inputs_shared,
415
+ inputs_posi,
416
+ )
417
+ fake_score_loss = F.mse_loss(fake_score_x0.float(), gen_data.float(), reduction="mean")
418
+
419
+ gan_loss_disc = torch.zeros_like(fake_score_loss)
420
+ gan_loss_ar1 = torch.zeros_like(fake_score_loss)
421
+ gan_loss_logit_reg = torch.zeros_like(fake_score_loss)
422
+ if self.config.gan_loss_weight > 0:
423
+ with torch.no_grad():
424
+ fake_feat = self._model_forward_features(
425
+ module,
426
+ get_dmd2_teacher_model(module),
427
+ module.dmd2_model_fn_teacher,
428
+ x_t_sg,
429
+ timestep,
430
+ inputs_shared,
431
+ inputs_posi,
432
+ )
433
+ real_feat, real_timestep, real_sigma = self._compute_real_feat(
434
+ module, real_data, timestep, sigma, eps, inputs_shared, inputs_posi
435
+ )
436
+ discriminator = get_dmd2_discriminator(module)
437
+ real_logits = discriminator(real_feat)
438
+ fake_logits = discriminator(fake_feat)
439
+ gan_loss_disc = _gan_loss_discriminator(real_logits, fake_logits)
440
+ if self.config.gan_r1_reg_weight > 0:
441
+ perturbed_real_alpha = real_data + self.config.gan_r1_reg_alpha * torch.randn_like(real_data)
442
+ with torch.no_grad():
443
+ real_feat_alpha = self._model_forward_features(
444
+ module,
445
+ get_dmd2_teacher_model(module),
446
+ module.dmd2_model_fn_teacher,
447
+ perturbed_real_alpha,
448
+ real_timestep,
449
+ inputs_shared,
450
+ inputs_posi,
451
+ )
452
+ real_logits_alpha = discriminator(real_feat_alpha)
453
+ gan_loss_ar1 = F.mse_loss(real_logits, real_logits_alpha, reduction="mean")
454
+
455
+ loss = (
456
+ fake_score_loss
457
+ + gan_loss_disc
458
+ + self.config.gan_r1_reg_weight * gan_loss_ar1
459
+ )
460
+ return {"total_loss": loss}
461
+
462
+ def __call__(self, module, inputs, iteration):
463
+ inputs_shared, inputs_posi, inputs_nega = inputs
464
+ real_data = inputs_shared.get("input_latents")
465
+ if real_data is None:
466
+ raise ValueError("DMD2 requires image latents from the dataset. Please provide training images.")
467
+ student_phase = iteration % self.config.student_update_freq == 0
468
+ set_dmd2_train_phase(module, student_phase)
469
+ if student_phase:
470
+ return self._student_update_step(module, real_data, inputs_shared, inputs_posi, inputs_nega)
471
+ return self._fake_score_discriminator_update_step(module, real_data, inputs_shared, inputs_posi, inputs_nega)
472
+
473
+
474
+ def _trainable_params(module):
475
+ return [param for param in module.parameters() if param.requires_grad]
476
+
477
+
478
+ def _dmd2_current_optimizers(config, optimizers, iteration):
479
+ if iteration % config.student_update_freq == 0:
480
+ return [optimizers["student"]], [optimizers["student_scheduler"]]
481
+ current_optimizers = [optimizers["fake_score"]]
482
+ current_schedulers = [optimizers["fake_score_scheduler"]]
483
+ if "discriminator" in optimizers:
484
+ current_optimizers.append(optimizers["discriminator"])
485
+ current_schedulers.append(optimizers["discriminator_scheduler"])
486
+ return current_optimizers, current_schedulers
487
+
488
+
489
+ def launch_dmd2_training_task(
490
+ accelerator: Accelerator,
491
+ dataset: torch.utils.data.Dataset,
492
+ model,
493
+ model_logger,
494
+ learning_rate: float = 1e-5,
495
+ weight_decay: float = 1e-2,
496
+ num_workers: int = 1,
497
+ save_steps: int = None,
498
+ num_epochs: int = 1,
499
+ enable_model_cpu_offload: bool = False,
500
+ enable_optimizer_cpu_offload: bool = False,
501
+ cpu_offload_split_threshold: int = None,
502
+ customized_optimizer: str = None,
503
+ args=None,
504
+ **kwargs,
505
+ ):
506
+ if args is not None:
507
+ learning_rate = args.learning_rate
508
+ weight_decay = args.weight_decay
509
+ num_workers = args.dataset_num_workers
510
+ save_steps = args.save_steps
511
+ num_epochs = args.num_epochs
512
+ enable_model_cpu_offload = args.enable_model_cpu_offload
513
+ enable_optimizer_cpu_offload = args.enable_optimizer_cpu_offload
514
+ cpu_offload_split_threshold = args.cpu_offload_split_threshold
515
+ customized_optimizer = args.customized_optimizer
516
+
517
+ optimizer_class = get_optimizer_class(customized_optimizer)
518
+ config = model.dmd2_config
519
+ fake_score_lr = config.fake_score_learning_rate or learning_rate
520
+ discriminator_lr = config.discriminator_learning_rate or learning_rate
521
+
522
+ student_model = get_dmd2_student_model(model)
523
+ fake_score_model = get_dmd2_fake_score_model(model)
524
+ discriminator = get_dmd2_discriminator(model)
525
+ has_discriminator = discriminator is not None
526
+
527
+ student_optimizer = optimizer_class(_trainable_params(student_model), lr=learning_rate, weight_decay=weight_decay)
528
+ fake_score_optimizer = optimizer_class(fake_score_model.parameters(), lr=fake_score_lr, weight_decay=weight_decay)
529
+ student_scheduler = torch.optim.lr_scheduler.ConstantLR(student_optimizer, factor=1.0, total_iters=1)
530
+ fake_score_scheduler = torch.optim.lr_scheduler.ConstantLR(fake_score_optimizer, factor=1.0, total_iters=1)
531
+
532
+ optimizers = {
533
+ "student": student_optimizer,
534
+ "fake_score": fake_score_optimizer,
535
+ "student_scheduler": student_scheduler,
536
+ "fake_score_scheduler": fake_score_scheduler,
537
+ }
538
+ prepare_items = [model, student_optimizer, fake_score_optimizer, student_scheduler, fake_score_scheduler]
539
+ if has_discriminator:
540
+ discriminator_optimizer = optimizer_class(discriminator.parameters(), lr=discriminator_lr, weight_decay=weight_decay)
541
+ discriminator_scheduler = torch.optim.lr_scheduler.ConstantLR(discriminator_optimizer, factor=1.0, total_iters=1)
542
+ optimizers["discriminator"] = discriminator_optimizer
543
+ optimizers["discriminator_scheduler"] = discriminator_scheduler
544
+ prepare_items.extend([discriminator_optimizer, discriminator_scheduler])
545
+
546
+ dataloader = torch.utils.data.DataLoader(dataset, shuffle=True, collate_fn=lambda x: x[0], num_workers=num_workers)
547
+ prepare_items.append(dataloader)
548
+
549
+ if enable_model_cpu_offload:
550
+ prepared = accelerator.prepare(*prepare_items[1:])
551
+ model.pipe.device = accelerator.device
552
+ offload_manager = OffloadTrainingManager(
553
+ model,
554
+ accelerator.device,
555
+ enable_optimizer_cpu_offload,
556
+ cpu_offload_split_threshold,
557
+ )
558
+ prepared_tail = list(prepared)
559
+ else:
560
+ model.to(device=accelerator.device)
561
+ prepared = accelerator.prepare(*prepare_items)
562
+ model = prepared[0]
563
+ prepared_tail = list(prepared[1:])
564
+
565
+ optimizers["student"] = prepared_tail.pop(0)
566
+ optimizers["fake_score"] = prepared_tail.pop(0)
567
+ optimizers["student_scheduler"] = prepared_tail.pop(0)
568
+ optimizers["fake_score_scheduler"] = prepared_tail.pop(0)
569
+ if has_discriminator:
570
+ optimizers["discriminator"] = prepared_tail.pop(0)
571
+ optimizers["discriminator_scheduler"] = prepared_tail.pop(0)
572
+ dataloader = prepared_tail.pop(0)
573
+
574
+ initialize_deepspeed_gradient_checkpointing(accelerator)
575
+ iteration = 0
576
+ for epoch_id in range(num_epochs):
577
+ for data in tqdm(dataloader):
578
+ with accelerator.accumulate(model):
579
+ if dataset.load_from_cache:
580
+ loss_map = model({}, inputs=data, iteration=iteration)
581
+ else:
582
+ loss_map = model(data, iteration=iteration)
583
+ loss = loss_map["total_loss"]
584
+ current_optimizers, current_schedulers = _dmd2_current_optimizers(config, optimizers, iteration)
585
+ accelerator.backward(loss)
586
+ if enable_model_cpu_offload:
587
+ offload_manager.after_backward()
588
+ if iteration % config.student_update_freq == 0 and config.student_grad_clip_norm is not None and config.student_grad_clip_norm > 0:
589
+ accelerator.clip_grad_norm_(_trainable_params(get_dmd2_student_model(accelerator.unwrap_model(model))), config.student_grad_clip_norm)
590
+ for optimizer in current_optimizers:
591
+ optimizer.step()
592
+ for scheduler in current_schedulers:
593
+ scheduler.step()
594
+ for optimizer in current_optimizers:
595
+ optimizer.zero_grad(set_to_none=True)
596
+ model_logger.on_step_end(accelerator, model, save_steps, loss=loss)
597
+ iteration += 1
598
+ if save_steps is None:
599
+ model_logger.on_epoch_end(accelerator, model, epoch_id)
600
+
601
+ model_logger.on_training_end(accelerator, model, save_steps)
diffsynth/diffusion/flow_match.py ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch, math
2
+ import numpy as np
3
+ from typing_extensions import Literal
4
+
5
+
6
+ class FlowMatchScheduler():
7
+
8
+ def __init__(self, template: Literal["FLUX.1", "Wan", "Qwen-Image", "FLUX.2", "Z-Image", "LTX-2", "Qwen-Image-Lightning", "ERNIE-Image", "ACE-Step", "Ideogram4", "Krea-2", "Boogu", "MiniMax-H3", "MiniMax-Music3", "LingBot-Video"] = "FLUX.1"):
9
+ self.set_timesteps_fn = {
10
+ "FLUX.1": FlowMatchScheduler.set_timesteps_flux,
11
+ "Wan": FlowMatchScheduler.set_timesteps_wan,
12
+ "Qwen-Image": FlowMatchScheduler.set_timesteps_qwen_image,
13
+ "FLUX.2": FlowMatchScheduler.set_timesteps_flux2,
14
+ "Z-Image": FlowMatchScheduler.set_timesteps_z_image,
15
+ "LTX-2": FlowMatchScheduler.set_timesteps_ltx2,
16
+ "Qwen-Image-Lightning": FlowMatchScheduler.set_timesteps_qwen_image_lightning,
17
+ "ERNIE-Image": FlowMatchScheduler.set_timesteps_ernie_image,
18
+ "ACE-Step": FlowMatchScheduler.set_timesteps_ace_step,
19
+ "HiDream-O1-Image": FlowMatchScheduler.set_timesteps_hidream_o1_image,
20
+ "Ideogram4": FlowMatchScheduler.set_timesteps_ideogram4,
21
+ "Krea-2": FlowMatchScheduler.set_timesteps_krea2,
22
+ "Boogu": FlowMatchScheduler.set_timesteps_boogu,
23
+ "MiniMax-H3": FlowMatchScheduler.set_timesteps_minimax_h3,
24
+ "MiniMax-Music3": FlowMatchScheduler.set_timesteps_minimax_music3,
25
+ "LingBot-Video": FlowMatchScheduler.set_timesteps_lingbot_video,
26
+ }.get(template, FlowMatchScheduler.set_timesteps_flux)
27
+ self.num_train_timesteps = 1000
28
+
29
+ @staticmethod
30
+ def set_timesteps_flux(num_inference_steps=100, denoising_strength=1.0, shift=None):
31
+ sigma_min = 0.003/1.002
32
+ sigma_max = 1.0
33
+ shift = 3 if shift is None else shift
34
+ num_train_timesteps = 1000
35
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
36
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps)
37
+ sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
38
+ timesteps = sigmas * num_train_timesteps
39
+ return sigmas, timesteps
40
+
41
+ @staticmethod
42
+ def set_timesteps_wan(num_inference_steps=100, denoising_strength=1.0, shift=None):
43
+ sigma_min = 0.0
44
+ sigma_max = 1.0
45
+ shift = 5 if shift is None else shift
46
+ num_train_timesteps = 1000
47
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
48
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps + 1)[:-1]
49
+ sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
50
+ timesteps = sigmas * num_train_timesteps
51
+ return sigmas, timesteps
52
+
53
+ @staticmethod
54
+ def _calculate_shift_qwen_image(image_seq_len, base_seq_len=256, max_seq_len=8192, base_shift=0.5, max_shift=0.9):
55
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
56
+ b = base_shift - m * base_seq_len
57
+ mu = image_seq_len * m + b
58
+ return mu
59
+
60
+ @staticmethod
61
+ def set_timesteps_qwen_image(num_inference_steps=100, denoising_strength=1.0, exponential_shift_mu=None, dynamic_shift_len=None):
62
+ sigma_min = 0.0
63
+ sigma_max = 1.0
64
+ num_train_timesteps = 1000
65
+ shift_terminal = 0.02
66
+ # Sigmas
67
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
68
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps + 1)[:-1]
69
+ # Mu
70
+ if exponential_shift_mu is not None:
71
+ mu = exponential_shift_mu
72
+ elif dynamic_shift_len is not None:
73
+ mu = FlowMatchScheduler._calculate_shift_qwen_image(dynamic_shift_len)
74
+ else:
75
+ mu = 0.8
76
+ sigmas = math.exp(mu) / (math.exp(mu) + (1 / sigmas - 1))
77
+ # Shift terminal
78
+ one_minus_z = 1 - sigmas
79
+ scale_factor = one_minus_z[-1] / (1 - shift_terminal)
80
+ sigmas = 1 - (one_minus_z / scale_factor)
81
+ # Timesteps
82
+ timesteps = sigmas * num_train_timesteps
83
+ return sigmas, timesteps
84
+
85
+ @staticmethod
86
+ def set_timesteps_lingbot_video(num_inference_steps=100, denoising_strength=1.0, shift=None, t_thresh=None, sigma_tail_steps=0):
87
+ sigma_min = 0.0
88
+ sigma_max = 1.0
89
+ shift = 5 if shift is None else shift
90
+ num_train_timesteps = 1000
91
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
92
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps + 1)[:-1]
93
+ sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
94
+ if t_thresh is not None:
95
+ # Refinement schedule: keep the sub-threshold part of the shifted grid, pin the first
96
+ # sigma exactly at t_thresh, then append extra low-noise steps that end at sigma_min.
97
+ sigmas = sigmas[sigmas <= t_thresh + 1e-6]
98
+ if sigmas.numel() == 0 or abs(float(sigmas[0]) - t_thresh) > 1e-6:
99
+ sigmas = torch.cat([torch.tensor([t_thresh], dtype=sigmas.dtype), sigmas])
100
+ if sigma_tail_steps > 0:
101
+ tail_start = float(sigmas[-1])
102
+ tail = torch.linspace(tail_start, min(sigma_min, tail_start), sigma_tail_steps + 2)[1:-1]
103
+ sigmas = torch.cat([sigmas, tail.to(dtype=sigmas.dtype)])
104
+ timesteps = sigmas * num_train_timesteps
105
+ return sigmas, timesteps
106
+
107
+ @staticmethod
108
+ def set_timesteps_qwen_image_lightning(num_inference_steps=100, denoising_strength=1.0, exponential_shift_mu=None, dynamic_shift_len=None):
109
+ sigma_min = 0.0
110
+ sigma_max = 1.0
111
+ num_train_timesteps = 1000
112
+ base_shift = math.log(3)
113
+ max_shift = math.log(3)
114
+ # Sigmas
115
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
116
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps + 1)[:-1]
117
+ # Mu
118
+ if exponential_shift_mu is not None:
119
+ mu = exponential_shift_mu
120
+ elif dynamic_shift_len is not None:
121
+ mu = FlowMatchScheduler._calculate_shift_qwen_image(dynamic_shift_len, base_shift=base_shift, max_shift=max_shift)
122
+ else:
123
+ mu = 0.8
124
+ sigmas = math.exp(mu) / (math.exp(mu) + (1 / sigmas - 1))
125
+ # Timesteps
126
+ timesteps = sigmas * num_train_timesteps
127
+ return sigmas, timesteps
128
+
129
+ @staticmethod
130
+ def compute_empirical_mu(image_seq_len, num_steps):
131
+ a1, b1 = 8.73809524e-05, 1.89833333
132
+ a2, b2 = 0.00016927, 0.45666666
133
+
134
+ if image_seq_len > 4300:
135
+ mu = a2 * image_seq_len + b2
136
+ return float(mu)
137
+
138
+ m_200 = a2 * image_seq_len + b2
139
+ m_10 = a1 * image_seq_len + b1
140
+
141
+ a = (m_200 - m_10) / 190.0
142
+ b = m_200 - 200.0 * a
143
+ mu = a * num_steps + b
144
+
145
+ return float(mu)
146
+
147
+ @staticmethod
148
+ def set_timesteps_flux2(num_inference_steps=100, denoising_strength=1.0, dynamic_shift_len=None):
149
+ sigma_min = 1 / num_inference_steps
150
+ sigma_max = 1.0
151
+ num_train_timesteps = 1000
152
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
153
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps)
154
+ if dynamic_shift_len is None:
155
+ # If you ask me why I set mu=0.8,
156
+ # I can only say that it yields better training results.
157
+ mu = 0.8
158
+ else:
159
+ mu = FlowMatchScheduler.compute_empirical_mu(dynamic_shift_len, num_inference_steps)
160
+ sigmas = math.exp(mu) / (math.exp(mu) + (1 / sigmas - 1))
161
+ timesteps = sigmas * num_train_timesteps
162
+ return sigmas, timesteps
163
+
164
+ @staticmethod
165
+ def set_timesteps_ernie_image(num_inference_steps=50, denoising_strength=1.0, shift=3.0):
166
+ sigma_min = 0.0
167
+ sigma_max = 1.0
168
+ num_train_timesteps = 1000
169
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
170
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps + 1)[:-1]
171
+ if shift is not None and shift != 1.0:
172
+ sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
173
+ timesteps = sigmas * num_train_timesteps
174
+ return sigmas, timesteps
175
+
176
+ @staticmethod
177
+ def set_timesteps_ace_step(num_inference_steps=8, denoising_strength=1.0, shift=3.0):
178
+ num_train_timesteps = 1000
179
+ sigma_start = denoising_strength
180
+ sigmas = torch.linspace(sigma_start, 0.0, num_inference_steps + 1)[:-1]
181
+ if shift is not None and shift != 1.0:
182
+ sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
183
+ timesteps = sigmas * num_train_timesteps
184
+ return sigmas, timesteps
185
+
186
+ @staticmethod
187
+ def set_timesteps_z_image(num_inference_steps=100, denoising_strength=1.0, shift=None, target_timesteps=None):
188
+ sigma_min = 0.0
189
+ sigma_max = 1.0
190
+ shift = 3 if shift is None else shift
191
+ num_train_timesteps = 1000
192
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
193
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps + 1)[:-1]
194
+ sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
195
+ timesteps = sigmas * num_train_timesteps
196
+ if target_timesteps is not None:
197
+ target_timesteps = target_timesteps.to(dtype=timesteps.dtype, device=timesteps.device)
198
+ for timestep in target_timesteps:
199
+ timestep_id = torch.argmin((timesteps - timestep).abs())
200
+ timesteps[timestep_id] = timestep
201
+ return sigmas, timesteps
202
+
203
+ @staticmethod
204
+ def set_timesteps_joyai_image(num_inference_steps=100, denoising_strength=1.0, shift=None):
205
+ sigma_min = 0.0
206
+ sigma_max = 1.0
207
+ shift = 4.0 if shift is None else shift
208
+ num_train_timesteps = 1000
209
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
210
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps + 1)[:-1]
211
+ sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
212
+ timesteps = sigmas * num_train_timesteps
213
+ return sigmas, timesteps
214
+
215
+ @staticmethod
216
+ def set_timesteps_hidream_o1_image(num_inference_steps=28, denoising_strength=1.0, shift=None, special_case=None, **kwargs):
217
+ num_train_timesteps = 1000
218
+ shift = 3.0 if shift is None else shift
219
+ if special_case == "dev":
220
+ timesteps_list = [
221
+ 999, 987, 974, 960, 945, 929, 913, 895, 877, 857, 836, 814, 790, 764, 737,
222
+ 707, 675, 640, 602, 560, 515, 464, 409, 347, 278, 199, 110, 8,
223
+ ]
224
+ sigmas = torch.tensor([t / 1000.0 for t in timesteps_list], dtype=torch.float32)
225
+ timesteps = torch.tensor(timesteps_list, dtype=torch.float32)
226
+ return sigmas, timesteps
227
+ else:
228
+ sigma_min = 0.0
229
+ sigma_max = 1.0
230
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
231
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps + 1)[:-1]
232
+ sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
233
+ timesteps = sigmas * num_train_timesteps
234
+ return sigmas, timesteps
235
+
236
+ @staticmethod
237
+ def set_timesteps_ideogram4(num_inference_steps=50, denoising_strength=1.0, image_resolution=(1024, 1024), mu=0.0, std=1.5):
238
+ num_pixels = image_resolution[0] * image_resolution[1]
239
+ known_pixels = 512 * 512
240
+ mean = mu + 0.5 * math.log(num_pixels / known_pixels)
241
+ logsnr_min = -15.0
242
+ logsnr_max = 18.0
243
+ t_min = 1.0 / (1 + math.exp(0.5 * logsnr_max))
244
+ t_max = 1.0 / (1 + math.exp(0.5 * logsnr_min))
245
+ step_intervals = torch.linspace(0.0, denoising_strength, num_inference_steps + 1, dtype=torch.float64)
246
+ sigmas = []
247
+ for i in range(num_inference_steps + 1):
248
+ z = torch.special.ndtri(step_intervals[i])
249
+ y = mean + std * z
250
+ t_ = torch.special.expit(y)
251
+ t_ = 1 - t_
252
+ t_ = t_.clamp(t_min, t_max)
253
+ sigmas.append(float(t_.to(torch.float32)))
254
+ sigmas = torch.tensor(sigmas, dtype=torch.float32)
255
+ one_minus_t = (1 - sigmas)[:-1].flip(0)
256
+ sigma_start = one_minus_t[0] * denoising_strength
257
+ if one_minus_t[0] > 0:
258
+ one_minus_t = one_minus_t * (sigma_start / one_minus_t[0])
259
+ sigmas = sigmas.flip(dims=(0,))
260
+ timesteps = sigmas[:-1]
261
+ sigmas = (1 - sigmas)[:-1]
262
+ return sigmas, timesteps
263
+
264
+ def set_timesteps_boogu(num_inference_steps=50, denoising_strength=1.0, sigmas=None):
265
+ if sigmas is not None:
266
+ sigmas = torch.tensor(sigmas, dtype=torch.float32)
267
+ timesteps = 1 - sigmas
268
+ return sigmas, timesteps
269
+ t_arr = np.linspace(1-denoising_strength, 1, num_inference_steps + 1, dtype=np.float32)
270
+ mu = 1.15
271
+ sigma = 1
272
+ eps = 1e-8
273
+ t1 = 1.0 - t_arr
274
+ t1 = np.clip(t1, eps, 1.0 - eps)
275
+ num = math.exp(mu)
276
+ denom = num + np.power(1.0 / t1 - 1.0, sigma)
277
+ y = num / denom
278
+ t_arr = 1.0 - y
279
+ timesteps = torch.from_numpy(t_arr).float()[:-1]
280
+ sigmas = 1 - timesteps
281
+ return sigmas, timesteps
282
+
283
+ @staticmethod
284
+ def set_timesteps_krea2(num_inference_steps=28, denoising_strength=1.0, dynamic_shift_len=None, y1=0.5, y2=1.15, mu=None):
285
+ x1 = 256
286
+ x2 = 6400
287
+ sigma = 1
288
+ ts = torch.linspace(denoising_strength, 0, num_inference_steps + 1)
289
+ if mu is None and dynamic_shift_len is None:
290
+ # Training
291
+ mu = 0.8
292
+ elif mu is None:
293
+ # Raw
294
+ slope = (y2 - y1) / (x2 - x1)
295
+ mu = slope * dynamic_shift_len + (y1 - slope * x1)
296
+ ts = math.exp(mu) / (math.exp(mu) + (1.0 / ts - 1.0) ** sigma)
297
+ sigmas, timesteps = ts[:-1], ts[:-1]
298
+ return sigmas, timesteps
299
+
300
+ @staticmethod
301
+ def set_timesteps_ltx2(num_inference_steps=100, denoising_strength=1.0, dynamic_shift_len=None, terminal=0.1, special_case=None):
302
+ num_train_timesteps = 1000
303
+ if special_case == "stage2":
304
+ sigmas = torch.Tensor([0.909375, 0.725, 0.421875])
305
+ elif special_case == "distilled_stage1":
306
+ sigmas = torch.Tensor([1.0, 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875])
307
+ else:
308
+ dynamic_shift_len = dynamic_shift_len or 4096
309
+ sigma_shift = FlowMatchScheduler._calculate_shift_qwen_image(
310
+ image_seq_len=dynamic_shift_len,
311
+ base_seq_len=1024,
312
+ max_seq_len=4096,
313
+ base_shift=0.95,
314
+ max_shift=2.05,
315
+ )
316
+ sigma_min = 0.0
317
+ sigma_max = 1.0
318
+ sigma_start = sigma_min + (sigma_max - sigma_min) * denoising_strength
319
+ sigmas = torch.linspace(sigma_start, sigma_min, num_inference_steps + 1)[:-1]
320
+ sigmas = math.exp(sigma_shift) / (math.exp(sigma_shift) + (1 / sigmas - 1))
321
+ # Shift terminal
322
+ one_minus_z = 1.0 - sigmas
323
+ scale_factor = one_minus_z[-1] / (1 - terminal)
324
+ sigmas = 1.0 - (one_minus_z / scale_factor)
325
+ timesteps = sigmas * num_train_timesteps
326
+ return sigmas, timesteps
327
+
328
+ @staticmethod
329
+ def set_timesteps_minimax_h3(num_inference_steps=50, denoising_strength=1.0, shift=2.22):
330
+ num_train_timesteps = 1000
331
+ base = torch.linspace(denoising_strength, 0.0, num_inference_steps+1, dtype=torch.float32)[:-1]
332
+ sigmas = shift * base / (1 + (shift - 1) * base)
333
+ timesteps = sigmas * num_train_timesteps
334
+ return sigmas, timesteps
335
+
336
+ @staticmethod
337
+ def set_timesteps_minimax_music3(num_inference_steps=30, denoising_strength=1.0):
338
+ num_train_timesteps = 1000
339
+ sigmas = torch.linspace(denoising_strength, denoising_strength / num_inference_steps, num_inference_steps)
340
+ timesteps = sigmas * num_train_timesteps
341
+ return sigmas, timesteps
342
+
343
+ def set_training_weight(self):
344
+ steps = 1000
345
+ x = self.sigmas * self.num_train_timesteps
346
+ y = torch.exp(-2 * ((x - steps / 2) / steps) ** 2)
347
+ y_shifted = y - y.min()
348
+ bsmntw_weighing = y_shifted * (steps / y_shifted.sum())
349
+ if len(self.timesteps) != 1000:
350
+ # This is an empirical formula.
351
+ bsmntw_weighing = bsmntw_weighing * (len(self.timesteps) / steps)
352
+ bsmntw_weighing = bsmntw_weighing + bsmntw_weighing[1]
353
+ self.linear_timesteps_weights = bsmntw_weighing
354
+
355
+ def set_timesteps(self, num_inference_steps=100, denoising_strength=1.0, training=False, **kwargs):
356
+ self.sigmas, self.timesteps = self.set_timesteps_fn(
357
+ num_inference_steps=num_inference_steps,
358
+ denoising_strength=denoising_strength,
359
+ **kwargs,
360
+ )
361
+ if training:
362
+ self.set_training_weight()
363
+ self.training = True
364
+ else:
365
+ self.training = False
366
+
367
+ def step(self, model_output, timestep, sample, to_final=False, **kwargs):
368
+ if isinstance(timestep, torch.Tensor):
369
+ timestep = timestep.cpu()
370
+ timestep_id = torch.argmin((self.timesteps - timestep).abs())
371
+ sigma = self.sigmas[timestep_id]
372
+ if to_final or timestep_id + 1 >= len(self.timesteps):
373
+ sigma_ = 0
374
+ else:
375
+ sigma_ = self.sigmas[timestep_id + 1]
376
+ prev_sample = sample + model_output * (sigma_ - sigma)
377
+ return prev_sample
378
+
379
+ def return_to_timestep(self, timestep, sample, sample_stablized):
380
+ if isinstance(timestep, torch.Tensor):
381
+ timestep = timestep.cpu()
382
+ timestep_id = torch.argmin((self.timesteps - timestep).abs())
383
+ sigma = self.sigmas[timestep_id]
384
+ model_output = (sample - sample_stablized) / sigma
385
+ return model_output
386
+
387
+ def add_noise(self, original_samples, noise, timestep):
388
+ if isinstance(timestep, torch.Tensor):
389
+ timestep = timestep.cpu()
390
+ timestep_id = torch.argmin((self.timesteps - timestep).abs())
391
+ sigma = self.sigmas[timestep_id]
392
+ sample = (1 - sigma) * original_samples + sigma * noise
393
+ return sample
394
+
395
+ def training_target(self, sample, noise, timestep):
396
+ target = noise - sample
397
+ return target
398
+
399
+ def training_weight(self, timestep):
400
+ timestep_id = torch.argmin((self.timesteps - timestep.to(self.timesteps.device)).abs())
401
+ weights = self.linear_timesteps_weights[timestep_id]
402
+ return weights
403
+
404
+
405
+ class HiDreamO1FlashScheduler(FlowMatchScheduler):
406
+
407
+ def __init__(self, noise_scale_start=7.5, noise_scale_end=7.5, noise_clip_std=2.5):
408
+ self.set_timesteps_fn = HiDreamO1FlashScheduler.set_timesteps_hidream_o1_image_dev
409
+ self.num_train_timesteps = 1000
410
+ self.noise_clip_std = noise_clip_std
411
+ num_steps = 28
412
+ self.noise_scale_schedule = [
413
+ noise_scale_start + (noise_scale_end - noise_scale_start) * i / (num_steps - 1)
414
+ for i in range(num_steps)
415
+ ]
416
+
417
+ @staticmethod
418
+ def set_timesteps_hidream_o1_image_dev(**kwargs):
419
+ timesteps_list = [
420
+ 999, 987, 974, 960, 945, 929, 913, 895, 877, 857, 836, 814, 790, 764, 737,
421
+ 707, 675, 640, 602, 560, 515, 464, 409, 347, 278, 199, 110, 8,
422
+ ]
423
+ sigmas = torch.tensor([t / 1000.0 for t in timesteps_list], dtype=torch.float32)
424
+ timesteps = torch.tensor(timesteps_list, dtype=torch.float32)
425
+ return sigmas, timesteps
426
+
427
+ def clip_noise(self, noise):
428
+ if self.noise_clip_std > 0:
429
+ noise_std = noise.std().item()
430
+ clip_val = self.noise_clip_std * noise_std
431
+ noise = noise.clamp(min=-clip_val, max=clip_val)
432
+ return noise
433
+
434
+ def step(self, model_output, timestep, sample):
435
+ timestep_id = torch.argmin((self.timesteps - timestep).abs())
436
+ sigma = self.sigmas[timestep_id]
437
+ sigma_ = self.sigmas[timestep_id + 1] if timestep_id + 1 < len(self.sigmas) else 0
438
+ denoised = sample - model_output * sigma
439
+
440
+ noise = self.clip_noise(torch.randn(denoised.shape, device=denoised.device, dtype=denoised.dtype))
441
+ sample = sigma_ * noise * self.noise_scale_schedule[timestep_id] + (1.0 - sigma_) * denoised
442
+ return sample
diffsynth/diffusion/logger.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, os, torch
2
+ from accelerate import Accelerator
3
+
4
+
5
+ class TensorBoardLogger:
6
+ def __init__(self, log_dir):
7
+ from torch.utils.tensorboard import SummaryWriter
8
+ self.writer = SummaryWriter(log_dir=log_dir)
9
+ print(f"TensorBoard is enabled. Run `tensorboard --logdir={log_dir}` to visualize the training progress.")
10
+
11
+ def log(self, key, value, step):
12
+ self.writer.add_scalar(key, value, step)
13
+
14
+ def close(self):
15
+ if self.writer is not None:
16
+ self.writer.close()
17
+
18
+
19
+ class SwanLabLogger:
20
+ def __init__(self, project_name="DiffSynth-Studio", log_dir=None):
21
+ import swanlab
22
+ project_name = os.environ.get("SWANLAB_PROJECT", project_name)
23
+ self.swanlab = swanlab
24
+ self.swanlab.init(project=project_name, logdir=log_dir)
25
+ print(f"SwanLab is enabled. Project: {project_name}")
26
+
27
+ def log(self, key, value, step):
28
+ self.swanlab.log({key: value}, step=step)
29
+
30
+ def close(self):
31
+ self.swanlab.finish()
32
+
33
+
34
+ class WandbLogger:
35
+ def __init__(self, project_name="DiffSynth-Studio", log_dir=None):
36
+ import wandb
37
+ project_name = os.environ.get("WANDB_PROJECT", project_name)
38
+ if log_dir is not None:
39
+ os.makedirs(log_dir, exist_ok=True)
40
+ self.wandb = wandb
41
+ config = None
42
+ if log_dir is not None:
43
+ training_args_path = os.path.join(os.path.dirname(log_dir), "training_args.json")
44
+ if os.path.isfile(training_args_path):
45
+ with open(training_args_path, "r", encoding="utf-8") as handle:
46
+ config = json.load(handle)
47
+ self.run = self.wandb.init(project=project_name, dir=log_dir, config=config)
48
+ print(f"Wandb is enabled. Project: {project_name}")
49
+
50
+ def log(self, key, value, step):
51
+ if isinstance(value, torch.Tensor):
52
+ value = value.detach().float().mean().item()
53
+ self.wandb.log({key: value}, step=step)
54
+
55
+ def log_metrics(self, metrics, step):
56
+ values = {
57
+ key: value.detach().float().mean().item() if isinstance(value, torch.Tensor) else value
58
+ for key, value in metrics.items()
59
+ }
60
+ self.wandb.log(values, step=step)
61
+
62
+ def close(self):
63
+ self.wandb.finish()
64
+
65
+ class CSVLogger:
66
+ def __init__(self, log_dir):
67
+ import csv
68
+ os.makedirs(log_dir, exist_ok=True)
69
+ self.path = os.path.join(log_dir, "loss.csv")
70
+ file_exists = os.path.isfile(self.path) and os.path.getsize(self.path) > 0
71
+ self.file = open(self.path, "a", encoding="utf-8", newline="")
72
+ self.writer = csv.DictWriter(self.file, fieldnames=["step", "key", "value"], lineterminator="\n")
73
+ if not file_exists:
74
+ self.writer.writeheader()
75
+ self.file.flush()
76
+
77
+ def log(self, key, value, step):
78
+ if isinstance(value, torch.Tensor):
79
+ value = value.detach().float().mean().item()
80
+ self.writer.writerow({"step": step, "key": key, "value": value})
81
+ self.file.flush()
82
+
83
+ def close(self):
84
+ if not self.file.closed:
85
+ self.file.close()
86
+
87
+
88
+ class ModelLogger:
89
+ def __init__(
90
+ self, output_path, remove_prefix_in_ckpt=None, state_dict_converter=lambda x: x,
91
+ enable_tensorboard_log=False,
92
+ enable_swanlab_log=False, swanlab_project="DiffSynth-Studio",
93
+ enable_wandb_log=False, wandb_project="DiffSynth-Studio",
94
+ enable_csv_log=False,
95
+ ):
96
+ self.output_path = output_path
97
+ self.remove_prefix_in_ckpt = remove_prefix_in_ckpt
98
+ self.state_dict_converter = state_dict_converter
99
+ self.num_steps = 0
100
+ # Loggers
101
+ self.enable_tensorboard_log = enable_tensorboard_log
102
+ self.enable_swanlab_log = enable_swanlab_log
103
+ self.swanlab_project = swanlab_project
104
+ self.enable_wandb_log = enable_wandb_log
105
+ self.wandb_project = wandb_project
106
+ self.enable_csv_log = enable_csv_log
107
+ self.loggers = []
108
+ self.loggers_initialized = False
109
+
110
+ def init_loggers(self):
111
+ if self.enable_tensorboard_log:
112
+ self.loggers.append(TensorBoardLogger(os.path.join(self.output_path, "tensorboard_log")))
113
+ if self.enable_swanlab_log:
114
+ self.loggers.append(SwanLabLogger(project_name=self.swanlab_project, log_dir=os.path.join(self.output_path, "swanlab_log")))
115
+ if self.enable_wandb_log:
116
+ self.loggers.append(WandbLogger(project_name=self.wandb_project, log_dir=os.path.join(self.output_path, "wandb_log")))
117
+ if self.enable_csv_log:
118
+ self.loggers.append(CSVLogger(self.output_path))
119
+ self.loggers_initialized = True
120
+
121
+ def on_step_end(self, accelerator: Accelerator, model: torch.nn.Module, save_steps=None, **kwargs):
122
+ self.num_steps += 1
123
+ loss = kwargs.get("loss")
124
+ metrics = dict(kwargs.get("metrics") or {})
125
+ if loss is not None:
126
+ metrics["train/loss"] = loss
127
+ if metrics:
128
+ self.log_metrics(accelerator, metrics, self.num_steps)
129
+ if save_steps is not None and self.num_steps % save_steps == 0:
130
+ self.save_model(accelerator, model, f"step-{self.num_steps}.safetensors")
131
+
132
+ def log_metrics(self, accelerator: Accelerator, metrics, step):
133
+ if not accelerator.is_main_process:
134
+ return
135
+ if not self.loggers_initialized:
136
+ self.init_loggers()
137
+ for logger in self.loggers:
138
+ if hasattr(logger, "log_metrics"):
139
+ logger.log_metrics(metrics, step)
140
+ else:
141
+ for key, value in metrics.items():
142
+ logger.log(key, value, step)
143
+
144
+ def on_epoch_end(self, accelerator: Accelerator, model: torch.nn.Module, epoch_id):
145
+ self.save_model(accelerator, model, f"epoch-{epoch_id}.safetensors")
146
+
147
+ def on_training_end(self, accelerator: Accelerator, model: torch.nn.Module, save_steps=None, skip_model_saving=False):
148
+ if not skip_model_saving and save_steps is not None and self.num_steps % save_steps != 0:
149
+ self.save_model(accelerator, model, f"step-{self.num_steps}.safetensors")
150
+ for logger in self.loggers:
151
+ logger.close()
152
+
153
+ def save_model(self, accelerator: Accelerator, model: torch.nn.Module, file_name):
154
+ accelerator.wait_for_everyone()
155
+ deepspeed_plugin = getattr(accelerator.state, "deepspeed_plugin", None)
156
+ deepspeed_config = getattr(deepspeed_plugin, "deepspeed_config", {}) or {}
157
+ zero_stage = deepspeed_config.get("zero_optimization", {}).get("stage")
158
+ if zero_stage == 3 and hasattr(model, "_zero3_consolidated_16bit_state_dict"):
159
+ # LoRA runs freeze the base model. Excluding frozen parameters here
160
+ # avoids materializing a complete 33B CPU state dict before the normal
161
+ # export_trainable_state_dict filter is applied.
162
+ state_dict = model._zero3_consolidated_16bit_state_dict(exclude_frozen_parameters=True)
163
+ else:
164
+ state_dict = accelerator.get_state_dict(model)
165
+ if accelerator.is_main_process:
166
+ state_dict = accelerator.unwrap_model(model).export_trainable_state_dict(state_dict, remove_prefix=self.remove_prefix_in_ckpt)
167
+ state_dict = self.state_dict_converter(state_dict)
168
+ os.makedirs(self.output_path, exist_ok=True)
169
+ path = os.path.join(self.output_path, file_name)
170
+ accelerator.save(state_dict, path, safe_serialization=True)
diffsynth/diffusion/loss.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .base_pipeline import BasePipeline
2
+ import torch
3
+
4
+
5
+ def FlowMatchSFTLoss(pipe: BasePipeline, **inputs):
6
+ if "lora" in inputs:
7
+ # Image-to-LoRA models need to load lora here.
8
+ pipe.clear_lora(verbose=0)
9
+ pipe.load_lora(pipe.dit, state_dict=inputs["lora"], hotload=True, verbose=0)
10
+
11
+ max_timestep_boundary = int(inputs.get("max_timestep_boundary", 1) * len(pipe.scheduler.timesteps))
12
+ min_timestep_boundary = int(inputs.get("min_timestep_boundary", 0) * len(pipe.scheduler.timesteps))
13
+
14
+ timestep_id = torch.randint(min_timestep_boundary, max_timestep_boundary, (1,))
15
+ timestep = pipe.scheduler.timesteps[timestep_id].to(dtype=pipe.torch_dtype, device=pipe.device)
16
+
17
+ noise = torch.randn_like(inputs["input_latents"]) * inputs.get("noise_scale", 1.0)
18
+ inputs["latents"] = pipe.scheduler.add_noise(inputs["input_latents"], noise, timestep)
19
+ training_target = pipe.scheduler.training_target(inputs["input_latents"], noise, timestep)
20
+
21
+ if "first_frame_latents" in inputs:
22
+ inputs["latents"][:, :, 0:1] = inputs["first_frame_latents"]
23
+
24
+ models = {name: getattr(pipe, name) for name in pipe.in_iteration_models}
25
+ noise_pred = pipe.model_fn(**models, **inputs, timestep=timestep)
26
+
27
+ if "first_frame_latents" in inputs:
28
+ noise_pred = noise_pred[:, :, 1:]
29
+ training_target = training_target[:, :, 1:]
30
+
31
+ loss = torch.nn.functional.mse_loss(noise_pred.float(), training_target.float())
32
+ loss = loss * pipe.scheduler.training_weight(timestep)
33
+ return loss
34
+
35
+
36
+ def FlowMatchSFTAudioVideoLoss(pipe: BasePipeline, **inputs):
37
+ max_timestep_boundary = int(inputs.get("max_timestep_boundary", 1) * len(pipe.scheduler.timesteps))
38
+ min_timestep_boundary = int(inputs.get("min_timestep_boundary", 0) * len(pipe.scheduler.timesteps))
39
+
40
+ timestep_id = torch.randint(min_timestep_boundary, max_timestep_boundary, (1,))
41
+ timestep = pipe.scheduler.timesteps[timestep_id].to(dtype=pipe.torch_dtype, device=pipe.device)
42
+
43
+ # video
44
+ noise = torch.randn_like(inputs["input_latents"])
45
+ inputs["video_latents"] = pipe.scheduler.add_noise(inputs["input_latents"], noise, timestep)
46
+ training_target = pipe.scheduler.training_target(inputs["input_latents"], noise, timestep)
47
+
48
+ # audio
49
+ if inputs.get("audio_input_latents") is not None:
50
+ audio_noise = torch.randn_like(inputs["audio_input_latents"])
51
+ inputs["audio_latents"] = pipe.scheduler.add_noise(inputs["audio_input_latents"], audio_noise, timestep)
52
+ training_target_audio = pipe.scheduler.training_target(inputs["audio_input_latents"], audio_noise, timestep)
53
+
54
+ models = {name: getattr(pipe, name) for name in pipe.in_iteration_models}
55
+ noise_pred, noise_pred_audio = pipe.model_fn(**models, **inputs, timestep=timestep)
56
+
57
+ loss = torch.nn.functional.mse_loss(noise_pred.float(), training_target.float())
58
+ loss = loss * pipe.scheduler.training_weight(timestep)
59
+ if inputs.get("audio_input_latents") is not None:
60
+ loss_audio = torch.nn.functional.mse_loss(noise_pred_audio.float(), training_target_audio.float())
61
+ loss_audio = loss_audio * pipe.scheduler.training_weight(timestep)
62
+ loss = loss + loss_audio
63
+ return loss
64
+
65
+
66
+ def FlowMatchSFTMiniMaxH3AudioVideoLoss(pipe: BasePipeline, training_cfg_scale: float = 1.0, audio_loss_weight: float = 1.0, inputs_nega: dict | None = None, **inputs):
67
+ max_timestep_boundary = int(inputs.get("max_timestep_boundary", 1) * len(pipe.scheduler.timesteps))
68
+ min_timestep_boundary = int(inputs.get("min_timestep_boundary", 0) * len(pipe.scheduler.timesteps))
69
+
70
+ timestep_id = torch.randint(min_timestep_boundary, max_timestep_boundary, (1,))
71
+ timestep_video = pipe.scheduler.timesteps[timestep_id].to(dtype=torch.float32, device=pipe.device)
72
+ timestep_audio = pipe.scheduler_audio.timesteps[timestep_id].to(dtype=torch.float32, device=pipe.device)
73
+
74
+ noise = torch.randn_like(inputs["input_latents"])
75
+ inputs["video_latents"] = pipe.scheduler.add_noise(inputs["input_latents"], noise, timestep_video)
76
+ training_target = pipe.scheduler.training_target(inputs["input_latents"], noise, timestep_video)
77
+
78
+ if "audio_input_latents" in inputs:
79
+ audio_noise = torch.randn_like(inputs["audio_input_latents"])
80
+ inputs["audio_latents"] = pipe.scheduler_audio.add_noise(inputs["audio_input_latents"], audio_noise, timestep_audio)
81
+ training_target_audio = pipe.scheduler_audio.training_target(inputs["audio_input_latents"], audio_noise, timestep_audio)
82
+
83
+ models = {name: getattr(pipe, name) for name in pipe.in_iteration_models}
84
+ if training_cfg_scale > 1.0:
85
+ if not inputs_nega:
86
+ raise ValueError(
87
+ "MiniMax-H3 CFG-aware training requires unconditional inputs. "
88
+ "When using split training, rebuild the data cache with the same "
89
+ "--training_cfg_scale value."
90
+ )
91
+ inputs_uncond = {**inputs, **inputs_nega}
92
+ inputs_uncond["use_gradient_checkpointing"] = False
93
+ inputs_uncond["use_gradient_checkpointing_offload"] = False
94
+ with torch.no_grad():
95
+ noise_pred_uncond, noise_pred_audio_uncond = pipe.model_fn(
96
+ **models, **inputs_uncond,
97
+ timestep_video=timestep_video, timestep_audio=timestep_audio,
98
+ )
99
+
100
+ noise_pred, noise_pred_audio = pipe.model_fn(
101
+ **models, **inputs,
102
+ timestep_video=timestep_video, timestep_audio=timestep_audio,
103
+ )
104
+
105
+ if training_cfg_scale > 1.0:
106
+ # The checkpoint's conditional prediction has CFG distilled into it.
107
+ # Rearrange the CFG equation to recover the raw velocity fitted to the
108
+ # standard flow-matching target, using the current model as the teacher.
109
+ noise_pred = (noise_pred + (training_cfg_scale - 1.0) * noise_pred_uncond) / training_cfg_scale
110
+ noise_pred_audio = (noise_pred_audio + (training_cfg_scale - 1.0) * noise_pred_audio_uncond) / training_cfg_scale
111
+
112
+ loss_video = torch.nn.functional.mse_loss(noise_pred.float(), training_target.float())
113
+ loss_video = loss_video * pipe.scheduler.training_weight(timestep_video)
114
+ loss = loss_video
115
+ loss_components = {"train/video_loss": loss_video}
116
+ if "audio_input_latents" in inputs:
117
+ loss_audio = torch.nn.functional.mse_loss(noise_pred_audio.float(), training_target_audio.float())
118
+ loss_audio = loss_audio * pipe.scheduler_audio.training_weight(timestep_audio) * audio_loss_weight
119
+ loss = loss + loss_audio
120
+ loss_components["train/audio_loss"] = loss_audio
121
+ # Backpropagation still uses the exact same summed scalar as before. The
122
+ # The runner detaches these values for ordinary logging. Keeping the live
123
+ # component scalars here also permits optional diagnostic grad-norm passes;
124
+ # the real optimizer update still uses only the summed scalar above.
125
+ return loss, loss_components
126
+
127
+
128
+ def DirectDistillLoss(pipe: BasePipeline, **inputs):
129
+ pipe.scheduler.set_timesteps(inputs["num_inference_steps"])
130
+ pipe.scheduler.training = True
131
+ models = {name: getattr(pipe, name) for name in pipe.in_iteration_models}
132
+ for progress_id, timestep in enumerate(pipe.scheduler.timesteps):
133
+ timestep = timestep.unsqueeze(0).to(dtype=pipe.torch_dtype, device=pipe.device)
134
+ noise_pred = pipe.model_fn(**models, **inputs, timestep=timestep, progress_id=progress_id)
135
+ inputs["latents"] = pipe.step(pipe.scheduler, progress_id=progress_id, noise_pred=noise_pred, **inputs)
136
+ loss = torch.nn.functional.mse_loss(inputs["latents"].float(), inputs["input_latents"].float())
137
+ return loss
138
+
139
+
140
+ class TrajectoryImitationLoss(torch.nn.Module):
141
+ def __init__(self):
142
+ super().__init__()
143
+ self.initialized = False
144
+
145
+ def initialize(self, device):
146
+ import lpips # TODO: remove it
147
+ self.loss_fn = lpips.LPIPS(net='alex').to(device)
148
+ self.initialized = True
149
+
150
+ def fetch_trajectory(self, pipe: BasePipeline, timesteps_student, inputs_shared, inputs_posi, inputs_nega, num_inference_steps, cfg_scale):
151
+ trajectory = [inputs_shared["latents"].clone()]
152
+
153
+ pipe.scheduler.set_timesteps(num_inference_steps, target_timesteps=timesteps_student)
154
+ models = {name: getattr(pipe, name) for name in pipe.in_iteration_models}
155
+ for progress_id, timestep in enumerate(pipe.scheduler.timesteps):
156
+ timestep = timestep.unsqueeze(0).to(dtype=pipe.torch_dtype, device=pipe.device)
157
+ noise_pred = pipe.cfg_guided_model_fn(
158
+ pipe.model_fn, cfg_scale,
159
+ inputs_shared, inputs_posi, inputs_nega,
160
+ **models, timestep=timestep, progress_id=progress_id
161
+ )
162
+ inputs_shared["latents"] = pipe.step(pipe.scheduler, progress_id=progress_id, noise_pred=noise_pred.detach(), **inputs_shared)
163
+
164
+ trajectory.append(inputs_shared["latents"].clone())
165
+ return pipe.scheduler.timesteps, trajectory
166
+
167
+ def align_trajectory(self, pipe: BasePipeline, timesteps_teacher, trajectory_teacher, inputs_shared, inputs_posi, inputs_nega, num_inference_steps, cfg_scale):
168
+ loss = 0
169
+ pipe.scheduler.set_timesteps(num_inference_steps, training=True)
170
+ models = {name: getattr(pipe, name) for name in pipe.in_iteration_models}
171
+ for progress_id, timestep in enumerate(pipe.scheduler.timesteps):
172
+ timestep = timestep.unsqueeze(0).to(dtype=pipe.torch_dtype, device=pipe.device)
173
+
174
+ progress_id_teacher = torch.argmin((timesteps_teacher - timestep).abs())
175
+ inputs_shared["latents"] = trajectory_teacher[progress_id_teacher]
176
+
177
+ noise_pred = pipe.cfg_guided_model_fn(
178
+ pipe.model_fn, cfg_scale,
179
+ inputs_shared, inputs_posi, inputs_nega,
180
+ **models, timestep=timestep, progress_id=progress_id
181
+ )
182
+
183
+ sigma = pipe.scheduler.sigmas[progress_id]
184
+ sigma_ = 0 if progress_id + 1 >= len(pipe.scheduler.timesteps) else pipe.scheduler.sigmas[progress_id + 1]
185
+ if progress_id + 1 >= len(pipe.scheduler.timesteps):
186
+ latents_ = trajectory_teacher[-1]
187
+ else:
188
+ progress_id_teacher = torch.argmin((timesteps_teacher - pipe.scheduler.timesteps[progress_id + 1]).abs())
189
+ latents_ = trajectory_teacher[progress_id_teacher]
190
+
191
+ denom = sigma_ - sigma
192
+ denom = torch.sign(denom) * torch.clamp(denom.abs(), min=1e-6)
193
+ target = (latents_ - inputs_shared["latents"]) / denom
194
+ loss = loss + torch.nn.functional.mse_loss(noise_pred.float(), target.float()) * pipe.scheduler.training_weight(timestep)
195
+ return loss
196
+
197
+ def compute_regularization(self, pipe: BasePipeline, trajectory_teacher, inputs_shared, inputs_posi, inputs_nega, num_inference_steps, cfg_scale):
198
+ inputs_shared["latents"] = trajectory_teacher[0]
199
+ pipe.scheduler.set_timesteps(num_inference_steps)
200
+ models = {name: getattr(pipe, name) for name in pipe.in_iteration_models}
201
+ for progress_id, timestep in enumerate(pipe.scheduler.timesteps):
202
+ timestep = timestep.unsqueeze(0).to(dtype=pipe.torch_dtype, device=pipe.device)
203
+ noise_pred = pipe.cfg_guided_model_fn(
204
+ pipe.model_fn, cfg_scale,
205
+ inputs_shared, inputs_posi, inputs_nega,
206
+ **models, timestep=timestep, progress_id=progress_id
207
+ )
208
+ inputs_shared["latents"] = pipe.step(pipe.scheduler, progress_id=progress_id, noise_pred=noise_pred.detach(), **inputs_shared)
209
+
210
+ image_pred = pipe.vae_decoder(inputs_shared["latents"])
211
+ image_real = pipe.vae_decoder(trajectory_teacher[-1])
212
+ loss = self.loss_fn(image_pred.float(), image_real.float())
213
+ return loss
214
+
215
+ def forward(self, pipe: BasePipeline, inputs_shared, inputs_posi, inputs_nega):
216
+ if not self.initialized:
217
+ self.initialize(pipe.device)
218
+ with torch.no_grad():
219
+ pipe.scheduler.set_timesteps(8)
220
+ timesteps_teacher, trajectory_teacher = self.fetch_trajectory(inputs_shared["teacher"], pipe.scheduler.timesteps, inputs_shared, inputs_posi, inputs_nega, 50, 2)
221
+ timesteps_teacher = timesteps_teacher.to(dtype=pipe.torch_dtype, device=pipe.device)
222
+ loss_1 = self.align_trajectory(pipe, timesteps_teacher, trajectory_teacher, inputs_shared, inputs_posi, inputs_nega, 8, 1)
223
+ loss_2 = self.compute_regularization(pipe, trajectory_teacher, inputs_shared, inputs_posi, inputs_nega, 8, 1)
224
+ loss = loss_1 + loss_2
225
+ return loss
diffsynth/diffusion/parsers.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+
3
+
4
+ def add_dataset_base_config(parser: argparse.ArgumentParser):
5
+ parser.add_argument("--dataset_base_path", type=str, default="", required=True, help="Base path of the dataset.")
6
+ parser.add_argument("--dataset_metadata_path", type=str, default=None, help="Path to the metadata file of the dataset.")
7
+ parser.add_argument("--dataset_repeat", type=int, default=1, help="Number of times to repeat the dataset per epoch.")
8
+ parser.add_argument("--dataset_num_workers", type=int, default=0, help="Number of workers for data loading.")
9
+ parser.add_argument("--data_file_keys", type=str, default="image,video", help="Data file keys in the metadata. Comma-separated.")
10
+ parser.add_argument("--dataset_cache_file_list", type=str, default=None, help="Optional newline-delimited .pth manifest. When set, train from this cache snapshot instead of recursively scanning every .pth under dataset_base_path.")
11
+ parser.add_argument("--validation_cache_file_list", type=str, default=None, help="Optional newline-delimited validation .pth manifest. Validation samples are never added to the training dataset.")
12
+ return parser
13
+
14
+ def add_image_size_config(parser: argparse.ArgumentParser):
15
+ parser.add_argument("--height", type=int, default=None, help="Height of images. Leave `height` and `width` empty to enable dynamic resolution.")
16
+ parser.add_argument("--width", type=int, default=None, help="Width of images. Leave `height` and `width` empty to enable dynamic resolution.")
17
+ parser.add_argument("--max_pixels", type=int, default=1024*1024, help="Maximum number of pixels per frame, used for dynamic resolution.")
18
+ return parser
19
+
20
+ def add_video_size_config(parser: argparse.ArgumentParser):
21
+ parser.add_argument("--height", type=int, default=None, help="Height of images. Leave `height` and `width` empty to enable dynamic resolution.")
22
+ parser.add_argument("--width", type=int, default=None, help="Width of images. Leave `height` and `width` empty to enable dynamic resolution.")
23
+ parser.add_argument("--max_pixels", type=int, default=1024*1024, help="Maximum number of pixels per frame, used for dynamic resolution.")
24
+ parser.add_argument("--num_frames", type=int, default=81, help="Number of frames per video. Frames are sampled from the video prefix.")
25
+ return parser
26
+
27
+ def add_model_config(parser: argparse.ArgumentParser):
28
+ parser.add_argument("--model_paths", type=str, default=None, help="Paths to load models. In JSON format.")
29
+ parser.add_argument("--model_id_with_origin_paths", type=str, default=None, help="Model ID with origin paths, e.g., Wan-AI/Wan2.1-T2V-1.3B:diffusion_pytorch_model*.safetensors. Comma-separated.")
30
+ parser.add_argument("--extra_inputs", default=None, type=str, help="Additional model inputs, comma-separated.")
31
+ parser.add_argument("--fp8_models", default=None, type=str, help="Models with FP8 precision, comma-separated.")
32
+ parser.add_argument("--offload_models", default=None, type=str, help="Models with offload, comma-separated. Only used in splited training.")
33
+ parser.add_argument("--resume_from_checkpoint", default=None, type=str, help="Resume training from checkpoint file. Only single model training is supported.")
34
+ return parser
35
+
36
+ def add_quant_config(parser: argparse.ArgumentParser):
37
+ parser.add_argument("--quant_options", type=str, default=None, help="Dynamically quantize loaded models. Semicolon-separated entries, each `<model_string>:<method>[/<exclude_modules>]`, where `<model_string>` matches an entry in `--model_paths`/`--model_id_with_origin_paths`, `method` is a registered method (e.g. bitsandbytes_nf4), and `exclude_modules` optionally lists layers kept in full precision.")
38
+ return parser
39
+
40
+ def add_training_config(parser: argparse.ArgumentParser):
41
+ parser.add_argument("--learning_rate", type=float, default=1e-4, help="Learning rate.")
42
+ parser.add_argument("--num_epochs", type=int, default=1, help="Number of epochs.")
43
+ parser.add_argument("--trainable_models", type=str, default=None, help="Models to train, e.g., dit, vae, text_encoder.")
44
+ parser.add_argument("--find_unused_parameters", default=False, action="store_true", help="Whether to find unused parameters in DDP.")
45
+ parser.add_argument("--weight_decay", type=float, default=0.01, help="Weight decay.")
46
+ parser.add_argument("--task", type=str, default="sft", required=False, help="Task type.")
47
+ parser.add_argument("--customized_optimizer", type=str, default=None, help="Customized optimizer, e.g., `bitsandbytes.optim.Adam8bit` and `torch.optim.Adam`. The default optimizer is `torch.optim.AdamW`.")
48
+ parser.add_argument("--max_train_steps", type=int, default=None, help="Optional hard limit on optimizer steps, useful for end-to-end smoke tests.")
49
+ parser.add_argument("--skip_model_saving", default=False, action="store_true", help="Run training without writing model checkpoints. Intended for smoke tests and profiling.")
50
+ parser.add_argument("--validation_steps", type=int, default=None, help="Run validation every N optimizer steps. Disabled when unset.")
51
+ parser.add_argument("--validation_at_start", default=False, action="store_true", help="Run validation once at global step 0 before the first optimizer update.")
52
+ parser.add_argument("--validation_num_inference_steps", type=int, default=50, help="Number of full denoising steps per validation sample.")
53
+ parser.add_argument("--validation_seed", type=int, default=42, help="Base noise seed for deterministic full-denoising validation.")
54
+ parser.add_argument("--validation_flow_shift", type=float, default=12.0, help="Video inference flow shift used during validation.")
55
+ parser.add_argument("--validation_audio_flow_shift", type=float, default=3.0, help="Audio inference flow shift used during validation.")
56
+ parser.add_argument("--validation_cfg_scale", type=float, default=1.0, help="Classifier-free guidance scale used during full-denoising validation.")
57
+ parser.add_argument("--validation_save_outputs", default=False, action="store_true", help="Decode and save each generated validation sample under output_path/validation.")
58
+ parser.add_argument("--log_component_grad_norms", default=False, action="store_true", help="Log total, video-loss, and audio-loss parameter gradient norms. Component norms require two diagnostic forward/backward passes and do not affect optimizer updates.")
59
+ parser.add_argument("--grad_norm_steps", type=int, default=1, help="Measure component gradient norms every N optimizer steps when --log_component_grad_norms is enabled.")
60
+ parser.add_argument("--skip_existing_cache", default=False, action="store_true", help="For data processing only: keep existing rank/local-index .pth files and process only missing outputs. Resume with the same world size and preprocessing settings.")
61
+ parser.add_argument("--skip_data_process_oom", default=False, action="store_true", help="For data processing only: record CUDA OOM samples and continue with the next sample instead of terminating the worker.")
62
+ parser.add_argument("--retry_data_process_oom", default=False, action="store_true", help="For data processing only: retry samples that already have an OOM marker instead of skipping them during cache resume.")
63
+ parser.add_argument("--skip_data_process_errors", default=False, action="store_true", help="For data processing only: record per-sample media loading errors and continue instead of terminating the worker.")
64
+ parser.add_argument("--retry_data_process_errors", default=False, action="store_true", help="For data processing only: retry samples that already have a data-error marker during cache resume.")
65
+ return parser
66
+
67
+ def add_output_config(parser: argparse.ArgumentParser):
68
+ parser.add_argument("--output_path", type=str, default="./models", help="Output save path.")
69
+ parser.add_argument("--remove_prefix_in_ckpt", type=str, default="pipe.dit.", help="Remove prefix in ckpt.")
70
+ parser.add_argument("--save_steps", type=int, default=None, help="Number of checkpoint saving invervals. If None, checkpoints will be saved every epoch.")
71
+ return parser
72
+
73
+ def add_lora_config(parser: argparse.ArgumentParser):
74
+ parser.add_argument("--lora_base_model", type=str, default=None, help="Which model LoRA is added to.")
75
+ parser.add_argument("--lora_target_modules", type=str, default="q,k,v,o,ffn.0,ffn.2", help="Which layers LoRA is added to.")
76
+ parser.add_argument("--lora_rank", type=int, default=32, help="Rank of LoRA.")
77
+ parser.add_argument("--lora_checkpoint", type=str, default=None, help="Path to the LoRA checkpoint. If provided, LoRA will be loaded from this checkpoint.")
78
+ parser.add_argument("--preset_lora_path", type=str, default=None, help="Path to the preset LoRA checkpoint. If provided, this LoRA will be fused to the base model.")
79
+ parser.add_argument("--preset_lora_model", type=str, default=None, help="Which model the preset LoRA is fused to.")
80
+ return parser
81
+
82
+ def add_gradient_config(parser: argparse.ArgumentParser):
83
+ parser.add_argument("--use_gradient_checkpointing", default=False, action="store_true", help="Whether to use gradient checkpointing.")
84
+ parser.add_argument("--use_gradient_checkpointing_offload", default=False, action="store_true", help="Whether to offload gradient checkpointing to CPU memory.")
85
+ parser.add_argument("--gradient_accumulation_steps", type=int, default=1, help="Gradient accumulation steps.")
86
+ return parser
87
+
88
+ def add_template_model_config(parser: argparse.ArgumentParser):
89
+ parser.add_argument("--template_model_id_or_path", type=str, default=None, help="Model ID of path of template models.")
90
+ parser.add_argument("--enable_lora_hot_loading", default=False, action="store_true", help="Whether to enable LoRA hot-loading. Only available for image-to-lora models.")
91
+ return parser
92
+
93
+ def add_offload_training_config(parser: argparse.ArgumentParser):
94
+ parser.add_argument("--enable_model_cpu_offload", default=False, action="store_true", help="Enable layer offload training. Weights are kept on CPU and loaded to GPU one layer at a time.")
95
+ parser.add_argument("--enable_optimizer_cpu_offload", default=False, action="store_true", help="When --enable_model_cpu_offload is enabled, run optimizer on CPU. All params are offloaded to CPU. Default is False (trainable params stay on GPU, optimizer on GPU).")
96
+ parser.add_argument("--cpu_offload_split_threshold", type=int, default=None, help="Experimental! When --enable_model_cpu_offload is enabled, modules with total params above this threshold (in MB) are recursively split into children. None means offload every leaf module directly. Default: None.")
97
+ return parser
98
+
99
+ def add_logger_config(parser: argparse.ArgumentParser):
100
+ parser.add_argument("--enable_tensorboard_log", default=False, action="store_true", help="Enable tensorboard for logging.")
101
+ parser.add_argument("--enable_swanlab_log", default=False, action="store_true", help="Enable swanlab for logging.")
102
+ parser.add_argument("--swanlab_project", type=str, default="DiffSynth-Studio", help="SwanLab project name.")
103
+ parser.add_argument("--enable_wandb_log", default=False, action="store_true", help="Enable wandb for logging.")
104
+ parser.add_argument("--wandb_project", type=str, default="DiffSynth-Studio", help="Wandb project name.")
105
+ parser.add_argument("--enable_csv_log", default=False, action="store_true", help="Enable CSV loss logging.")
106
+ return parser
107
+
108
+ def add_dmd2_config(parser: argparse.ArgumentParser):
109
+ parser.add_argument("--dmd2_student_update_freq", type=int, default=5, help="Update student once every N DMD2 iterations.")
110
+ parser.add_argument("--dmd2_student_sample_steps", type=int, default=4, help="Number of distilled student sampling steps.")
111
+ parser.add_argument("--dmd2_student_sample_type", type=str, default="sde", choices=["sde", "ode"], help="Student sampling type.")
112
+ parser.add_argument("--dmd2_student_schedule", type=str, default="uniform", choices=["uniform"], help="Student sigma schedule.")
113
+ parser.add_argument("--dmd2_student_t_list", type=str, default=None, help="Optional student sigma schedule, including the final 0.")
114
+ parser.add_argument("--dmd2_matching_t_min", type=float, default=0.001, help="Minimum matching sigma sampled for DMD2.")
115
+ parser.add_argument("--dmd2_matching_t_max", type=float, default=0.999, help="Maximum matching sigma sampled for DMD2.")
116
+ parser.add_argument("--dmd2_matching_t_sampling", type=str, default="uniform", choices=["uniform", "logitnormal"], help="Sample matching sigma.")
117
+ parser.add_argument("--dmd2_matching_t_mean", type=float, default=0.0, help="Mean for logitnormal matching timestep sampling.")
118
+ parser.add_argument("--dmd2_matching_t_std", type=float, default=1.0, help="Std for logitnormal matching timestep sampling.")
119
+ parser.add_argument("--dmd2_gan_loss_weight", type=float, default=0.03, help="Generator GAN loss weight.")
120
+ parser.add_argument("--dmd2_gan_r1_reg_weight", type=float, default=0.0, help="Approximate R1 regularization weight for the discriminator.")
121
+ parser.add_argument("--dmd2_gan_r1_reg_alpha", type=float, default=0.1, help="Noise scale for approximate R1 regularization.")
122
+ parser.add_argument("--dmd2_fake_score_learning_rate", type=float, default=None, help="Learning rate for the fake score model.")
123
+ parser.add_argument("--dmd2_discriminator_learning_rate", type=float, default=None, help="Learning rate for the discriminator.")
124
+ parser.add_argument("--dmd2_feature_indices", type=str, default=None, help="Model feature indices used by the DMD2 discriminator.")
125
+ parser.add_argument("--dmd2_teacher_cfg_scale", type=float, default=1.0, help="CFG scale.")
126
+ parser.add_argument("--dmd2_student_grad_clip_norm", type=float, default=10.0, help="Clip student gradients to this norm.")
127
+ return parser
128
+
129
+ def add_general_config(parser: argparse.ArgumentParser):
130
+ parser = add_dataset_base_config(parser)
131
+ parser = add_model_config(parser)
132
+ parser = add_quant_config(parser)
133
+ parser = add_training_config(parser)
134
+ parser = add_output_config(parser)
135
+ parser = add_lora_config(parser)
136
+ parser = add_gradient_config(parser)
137
+ parser = add_template_model_config(parser)
138
+ parser = add_offload_training_config(parser)
139
+ parser = add_logger_config(parser)
140
+ return parser
diffsynth/diffusion/runner.py ADDED
@@ -0,0 +1,645 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gc, os, json, time, torch, importlib, traceback
2
+ from tqdm import tqdm
3
+ from accelerate import Accelerator
4
+ from .training_module import DiffusionTrainingModule
5
+ from .logger import ModelLogger
6
+ from diffsynth.core import OffloadTrainingManager
7
+
8
+
9
+ def get_optimizer_class(customized_optimizer=None):
10
+ if customized_optimizer is None:
11
+ return torch.optim.AdamW
12
+ else:
13
+ module_name, class_name = customized_optimizer.rsplit(".", 1)
14
+ module = importlib.import_module(module_name)
15
+ print(f"Customized opimizer `{customized_optimizer}` imported.")
16
+ return getattr(module, class_name)
17
+
18
+
19
+ def _current_global_grad_norm(accelerator, model):
20
+ """Return the L2 norm of the current trainable gradients, including ZeRO-3 shards."""
21
+ parameters = [parameter for parameter in model.parameters() if parameter.requires_grad]
22
+ uses_zero3 = any(hasattr(parameter, "ds_id") for parameter in parameters)
23
+ squared_norm = torch.zeros((), dtype=torch.float64, device=accelerator.device)
24
+ found_gradient = False
25
+ if uses_zero3:
26
+ from deepspeed.utils import safe_get_local_grad
27
+ for parameter in parameters:
28
+ if not hasattr(parameter, "ds_id"):
29
+ continue
30
+ gradient = safe_get_local_grad(parameter)
31
+ if gradient is not None:
32
+ squared_norm += gradient.detach().double().square().sum()
33
+ found_gradient = True
34
+ squared_norm = accelerator.reduce(squared_norm, reduction="sum")
35
+ else:
36
+ # DDP gradients have already been averaged and are replicated, so do not
37
+ # sum the same full gradient once more across ranks.
38
+ for parameter in parameters:
39
+ if parameter.grad is not None:
40
+ squared_norm += parameter.grad.detach().double().square().sum()
41
+ found_gradient = True
42
+ if not found_gradient:
43
+ raise RuntimeError("No trainable gradients were available for grad-norm logging")
44
+ return squared_norm.sqrt().float()
45
+
46
+
47
+ def _forward_cached_training_loss(model, data, load_from_cache):
48
+ model_output = model({}, inputs=data) if load_from_cache else model(data)
49
+ if (
50
+ isinstance(model_output, tuple)
51
+ and len(model_output) == 2
52
+ and isinstance(model_output[1], dict)
53
+ ):
54
+ return model_output
55
+ return model_output, {}
56
+
57
+
58
+ def _diagnostic_component_grad_norms(accelerator, model, optimizer, data, load_from_cache):
59
+ """Measure component parameter gradients without applying an optimizer update.
60
+
61
+ Two fresh forward/backward passes avoid retaining the 33B-model graph. RNG
62
+ states are restored before each pass and again on exit, so these diagnostics
63
+ do not perturb subsequent training noise/timestep sampling.
64
+ """
65
+ component_keys = (
66
+ ("train/video_loss", "train/video_grad_norm"),
67
+ ("train/audio_loss", "train/audio_grad_norm"),
68
+ )
69
+ cpu_rng_state = torch.random.get_rng_state()
70
+ cuda_rng_state = None
71
+ if accelerator.device.type == "cuda":
72
+ cuda_rng_state = torch.cuda.get_rng_state(accelerator.device)
73
+ deepspeed_boundary = None
74
+ if hasattr(model, "set_gradient_accumulation_boundary"):
75
+ deepspeed_boundary = getattr(model, "_is_gradient_accumulation_boundary", None)
76
+ # Diagnostic passes must reduce their standalone gradients immediately,
77
+ # independent of the real training accumulation counter.
78
+ model.set_gradient_accumulation_boundary(True)
79
+ results = {}
80
+ try:
81
+ for component_key, metric_key in component_keys:
82
+ torch.random.set_rng_state(cpu_rng_state)
83
+ if cuda_rng_state is not None:
84
+ torch.cuda.set_rng_state(cuda_rng_state, accelerator.device)
85
+ optimizer.zero_grad()
86
+ _, components = _forward_cached_training_loss(model, data, load_from_cache)
87
+ component_loss = components.get(component_key)
88
+ if component_loss is None:
89
+ raise RuntimeError(
90
+ f"Model did not return {component_key}; cannot measure component grad norm"
91
+ )
92
+ backward_kwargs = {"scale_wrt_gas": False} if hasattr(
93
+ model, "set_gradient_accumulation_boundary"
94
+ ) else {}
95
+ accelerator.backward(component_loss, **backward_kwargs)
96
+ results[metric_key] = _current_global_grad_norm(accelerator, model).detach()
97
+ finally:
98
+ optimizer.zero_grad()
99
+ torch.random.set_rng_state(cpu_rng_state)
100
+ if cuda_rng_state is not None:
101
+ torch.cuda.set_rng_state(cuda_rng_state, accelerator.device)
102
+ if hasattr(model, "set_gradient_accumulation_boundary"):
103
+ model.set_gradient_accumulation_boundary(deepspeed_boundary)
104
+ return results
105
+
106
+
107
+ def save_training_args(args):
108
+ output_path = getattr(args, "output_path", None) if args is not None else None
109
+ if output_path is None:
110
+ return
111
+ try:
112
+ os.makedirs(args.output_path, exist_ok=True)
113
+ save_path = os.path.join(args.output_path, "training_args.json")
114
+ with open(save_path, "w", encoding="utf-8") as f:
115
+ json.dump(vars(args), f, indent=4, ensure_ascii=False, default=str)
116
+ print(f"Training arguments saved to `{save_path}`.")
117
+ except Exception as e:
118
+ print(f"Warning: failed to save training arguments: {e}")
119
+
120
+
121
+ def launch_training_task(
122
+ accelerator: Accelerator,
123
+ dataset: torch.utils.data.Dataset,
124
+ model: DiffusionTrainingModule,
125
+ model_logger: ModelLogger,
126
+ learning_rate: float = 1e-5,
127
+ weight_decay: float = 1e-2,
128
+ num_workers: int = 1,
129
+ save_steps: int = None,
130
+ num_epochs: int = 1,
131
+ enable_model_cpu_offload: bool = False,
132
+ enable_optimizer_cpu_offload: bool = False,
133
+ cpu_offload_split_threshold: int = None,
134
+ customized_optimizer: str = None,
135
+ max_train_steps: int = None,
136
+ skip_model_saving: bool = False,
137
+ validation_dataset=None,
138
+ args = None,
139
+ **kwargs,
140
+ ):
141
+ if args is not None:
142
+ learning_rate = args.learning_rate
143
+ weight_decay = args.weight_decay
144
+ num_workers = args.dataset_num_workers
145
+ save_steps = args.save_steps
146
+ num_epochs = args.num_epochs
147
+ enable_model_cpu_offload = args.enable_model_cpu_offload
148
+ enable_optimizer_cpu_offload = args.enable_optimizer_cpu_offload
149
+ cpu_offload_split_threshold = args.cpu_offload_split_threshold
150
+ customized_optimizer = args.customized_optimizer
151
+ max_train_steps = args.max_train_steps
152
+ skip_model_saving = args.skip_model_saving
153
+
154
+ if accelerator.is_main_process:
155
+ save_training_args(args)
156
+
157
+ optimizer_class = get_optimizer_class(customized_optimizer)
158
+ optimizer = optimizer_class(model.trainable_modules(), lr=learning_rate, weight_decay=weight_decay)
159
+ # DeepSpeed performs the optimizer update inside accelerator.backward().
160
+ # Keeping a separate PyTorch ConstantLR here would remain attached to the
161
+ # pre-DeepSpeed optimizer (and emit a misleading step-order warning), while
162
+ # the actual DeepSpeed optimizer already keeps a constant learning rate.
163
+ use_deepspeed = getattr(accelerator.state, "deepspeed_plugin", None) is not None
164
+ scheduler = None if use_deepspeed else torch.optim.lr_scheduler.ConstantLR(optimizer)
165
+ # Cached video/reference tensors have variable temporal and sequence shapes,
166
+ # so this training path intentionally uses one sample per rank.
167
+ dataloader = torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=True, collate_fn=lambda x: x[0], num_workers=num_workers)
168
+
169
+ if enable_model_cpu_offload:
170
+ if scheduler is None:
171
+ optimizer, dataloader = accelerator.prepare(optimizer, dataloader)
172
+ else:
173
+ optimizer, dataloader, scheduler = accelerator.prepare(optimizer, dataloader, scheduler)
174
+ model.pipe.device = accelerator.device
175
+ offload_manager = OffloadTrainingManager(model, accelerator.device, enable_optimizer_cpu_offload, cpu_offload_split_threshold)
176
+ else:
177
+ model.to(device=accelerator.device)
178
+ if scheduler is None:
179
+ model, optimizer, dataloader = accelerator.prepare(model, optimizer, dataloader)
180
+ else:
181
+ model, optimizer, dataloader, scheduler = accelerator.prepare(model, optimizer, dataloader, scheduler)
182
+
183
+ initialize_deepspeed_gradient_checkpointing(accelerator)
184
+ validation_steps = getattr(args, "validation_steps", None) if args is not None else None
185
+ validation_at_start = bool(getattr(args, "validation_at_start", False)) if args is not None else False
186
+ log_component_grad_norms = bool(
187
+ getattr(args, "log_component_grad_norms", False)
188
+ ) if args is not None else False
189
+ grad_norm_steps = int(getattr(args, "grad_norm_steps", 1)) if args is not None else 1
190
+ if validation_steps is not None and validation_steps <= 0:
191
+ raise ValueError("--validation_steps must be positive")
192
+ if (validation_at_start or validation_steps is not None) and validation_dataset is None:
193
+ raise ValueError("Validation was requested but --validation_cache_file_list was not provided")
194
+ if grad_norm_steps <= 0:
195
+ raise ValueError("--grad_norm_steps must be positive")
196
+ if validation_at_start:
197
+ run_full_denoise_validation(accelerator, model, validation_dataset, model_logger, step=0, args=args)
198
+
199
+ reached_max_steps = False
200
+ accumulated_loss = None
201
+ accumulated_loss_metrics = {}
202
+ accumulated_micro_steps = 0
203
+ for epoch_id in range(num_epochs):
204
+ progress_bar = tqdm(
205
+ dataloader,
206
+ desc=f"epoch {epoch_id + 1} micro-batches",
207
+ disable=not accelerator.is_local_main_process,
208
+ )
209
+ for data in progress_bar:
210
+ with accelerator.accumulate(model):
211
+ loss, loss_metrics = _forward_cached_training_loss(
212
+ model, data, dataset.load_from_cache,
213
+ )
214
+ accelerator.backward(loss)
215
+ if enable_model_cpu_offload:
216
+ offload_manager.after_backward()
217
+ measure_grad_norms = (
218
+ accelerator.sync_gradients
219
+ and log_component_grad_norms
220
+ and (model_logger.num_steps + 1) % grad_norm_steps == 0
221
+ )
222
+ grad_norm_metrics = {}
223
+ if measure_grad_norms:
224
+ # This is the exact accumulated gradient used by the real
225
+ # optimizer update below.
226
+ grad_norm_metrics["train/grad_norm"] = _current_global_grad_norm(
227
+ accelerator, model,
228
+ ).detach()
229
+ optimizer.step()
230
+ if scheduler is not None:
231
+ scheduler.step()
232
+ optimizer.zero_grad()
233
+ detached_loss = loss.detach()
234
+ accumulated_loss = detached_loss if accumulated_loss is None else accumulated_loss + detached_loss
235
+ for key, value in loss_metrics.items():
236
+ detached_value = value.detach()
237
+ accumulated_loss_metrics[key] = accumulated_loss_metrics.get(key, 0) + detached_value
238
+ accumulated_micro_steps += 1
239
+ # Only count/log/save/validate after a real optimizer update.
240
+ # With gradient accumulation, intermediate optimizer.step() calls
241
+ # are no-ops managed by Accelerate/DeepSpeed.
242
+ if accelerator.sync_gradients:
243
+ if measure_grad_norms:
244
+ grad_norm_metrics.update(_diagnostic_component_grad_norms(
245
+ accelerator, model, optimizer, data, dataset.load_from_cache,
246
+ ))
247
+ mean_loss = accumulated_loss / accumulated_micro_steps
248
+ mean_loss = accelerator.reduce(mean_loss, reduction="mean")
249
+ mean_loss_metrics = {
250
+ key: accelerator.reduce(value / accumulated_micro_steps, reduction="mean")
251
+ for key, value in accumulated_loss_metrics.items()
252
+ }
253
+ accumulated_loss = None
254
+ accumulated_loss_metrics = {}
255
+ accumulated_micro_steps = 0
256
+ mean_loss_metrics.update(grad_norm_metrics)
257
+ model_logger.on_step_end(
258
+ accelerator, model, save_steps,
259
+ loss=mean_loss, metrics=mean_loss_metrics,
260
+ )
261
+ progress_bar.set_postfix(optimizer_step=model_logger.num_steps)
262
+ if validation_steps is not None and model_logger.num_steps % validation_steps == 0:
263
+ run_full_denoise_validation(
264
+ accelerator, model, validation_dataset, model_logger,
265
+ step=model_logger.num_steps, args=args,
266
+ )
267
+ if max_train_steps is not None and model_logger.num_steps >= max_train_steps:
268
+ reached_max_steps = True
269
+ break
270
+ if not skip_model_saving and save_steps is None:
271
+ model_logger.on_epoch_end(accelerator, model, epoch_id)
272
+ if reached_max_steps:
273
+ break
274
+
275
+ report_peak_cuda_memory(accelerator, model_logger.output_path)
276
+ model_logger.on_training_end(accelerator, model, save_steps, skip_model_saving=skip_model_saving)
277
+ accelerator.end_training()
278
+
279
+
280
+ def _write_json_atomic(path, payload):
281
+ temp_path = f"{path}.tmp"
282
+ with open(temp_path, "w", encoding="utf-8") as handle:
283
+ json.dump(payload, handle, ensure_ascii=False, indent=2, default=str)
284
+ handle.write("\n")
285
+ os.replace(temp_path, path)
286
+
287
+
288
+ class _DataProcessShardDataset(torch.utils.data.Dataset):
289
+ """Shard data by rank and check cache files before decoding source media."""
290
+
291
+ def __init__(
292
+ self,
293
+ dataset,
294
+ output_path,
295
+ process_index,
296
+ num_processes,
297
+ skip_existing_cache=False,
298
+ retry_data_process_oom=False,
299
+ skip_data_process_errors=False,
300
+ retry_data_process_errors=False,
301
+ ):
302
+ self.dataset = dataset
303
+ self.folder = os.path.join(output_path, str(process_index))
304
+ self.process_index = process_index
305
+ self.num_processes = num_processes
306
+ self.skip_existing_cache = skip_existing_cache
307
+ self.retry_data_process_oom = retry_data_process_oom
308
+ self.skip_data_process_errors = skip_data_process_errors
309
+ self.retry_data_process_errors = retry_data_process_errors
310
+ os.makedirs(self.folder, exist_ok=True)
311
+
312
+ def __len__(self):
313
+ remaining = len(self.dataset) - self.process_index
314
+ return max(0, (remaining + self.num_processes - 1) // self.num_processes)
315
+
316
+ def cache_paths(self, local_data_id):
317
+ return (
318
+ os.path.join(self.folder, f"{local_data_id}.pth"),
319
+ os.path.join(self.folder, f"{local_data_id}.oom.json"),
320
+ os.path.join(self.folder, f"{local_data_id}.data_error.json"),
321
+ )
322
+
323
+ def __getitem__(self, local_data_id):
324
+ save_path, oom_marker_path, data_error_path = self.cache_paths(local_data_id)
325
+ if self.skip_existing_cache:
326
+ if os.path.isfile(save_path):
327
+ return local_data_id, "cache", None
328
+ if not self.retry_data_process_oom and os.path.isfile(oom_marker_path):
329
+ return local_data_id, "oom_marker", None
330
+ if not self.retry_data_process_errors and os.path.isfile(data_error_path):
331
+ return local_data_id, "data_error_marker", None
332
+
333
+ global_data_id = self.process_index + local_data_id * self.num_processes
334
+ try:
335
+ return local_data_id, None, self.dataset[global_data_id]
336
+ except Exception as error:
337
+ if not self.skip_data_process_errors:
338
+ raise
339
+ return local_data_id, "load_error", {
340
+ "rank": self.process_index,
341
+ "local_data_id": local_data_id,
342
+ "global_data_id": global_data_id,
343
+ "error_type": type(error).__name__,
344
+ "error": str(error),
345
+ "traceback": traceback.format_exc(),
346
+ "created_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
347
+ }
348
+
349
+
350
+ def launch_data_process_task(
351
+ accelerator: Accelerator,
352
+ dataset: torch.utils.data.Dataset,
353
+ model: DiffusionTrainingModule,
354
+ model_logger: ModelLogger,
355
+ num_workers: int = 8,
356
+ args = None,
357
+ **kwargs,
358
+ ):
359
+ if args is not None:
360
+ num_workers = args.dataset_num_workers
361
+ enable_model_cpu_offload = args.enable_model_cpu_offload
362
+ enable_optimizer_cpu_offload = args.enable_optimizer_cpu_offload
363
+ cpu_offload_split_threshold = args.cpu_offload_split_threshold
364
+ skip_existing_cache = args.skip_existing_cache
365
+ skip_data_process_oom = getattr(args, "skip_data_process_oom", False)
366
+ retry_data_process_oom = getattr(args, "retry_data_process_oom", False)
367
+ skip_data_process_errors = getattr(args, "skip_data_process_errors", False)
368
+ retry_data_process_errors = getattr(args, "retry_data_process_errors", False)
369
+ else:
370
+ enable_model_cpu_offload = False
371
+ enable_optimizer_cpu_offload = False
372
+ cpu_offload_split_threshold = None
373
+ skip_existing_cache = False
374
+ skip_data_process_oom = False
375
+ retry_data_process_oom = False
376
+ skip_data_process_errors = False
377
+ retry_data_process_errors = False
378
+
379
+ # Data processing has no cross-rank computation. Shard explicitly so cache
380
+ # existence can be checked before UnifiedDataset decodes video/audio data.
381
+ # This matches Accelerate's batch_size=1, split_batches=False mapping.
382
+ rank_dataset = _DataProcessShardDataset(
383
+ dataset,
384
+ model_logger.output_path,
385
+ accelerator.process_index,
386
+ accelerator.num_processes,
387
+ skip_existing_cache=skip_existing_cache,
388
+ retry_data_process_oom=retry_data_process_oom,
389
+ skip_data_process_errors=skip_data_process_errors,
390
+ retry_data_process_errors=retry_data_process_errors,
391
+ )
392
+ dataloader = torch.utils.data.DataLoader(
393
+ rank_dataset,
394
+ shuffle=False,
395
+ collate_fn=lambda items: items[0],
396
+ num_workers=num_workers,
397
+ )
398
+ if enable_model_cpu_offload:
399
+ offload_manager = OffloadTrainingManager(
400
+ model,
401
+ accelerator.device,
402
+ enable_optimizer_cpu_offload,
403
+ cpu_offload_split_threshold,
404
+ )
405
+ model.pipe.device = accelerator.device
406
+ else:
407
+ # Do not wrap the preprocessing model in DDP: ranks are independent and
408
+ # never synchronize gradients or buffers in this task.
409
+ model.to(device=accelerator.device)
410
+
411
+ processed_count = 0
412
+ skipped_cache_count = 0
413
+ skipped_oom_marker_count = 0
414
+ skipped_data_error_marker_count = 0
415
+ new_oom_count = 0
416
+ new_data_error_count = 0
417
+ progress_bar = tqdm(
418
+ dataloader,
419
+ desc=f"rank {accelerator.process_index}",
420
+ position=accelerator.local_process_index,
421
+ )
422
+ for data_id, status, data in progress_bar:
423
+ if status == "cache":
424
+ skipped_cache_count += 1
425
+ continue
426
+ if status == "oom_marker":
427
+ skipped_oom_marker_count += 1
428
+ continue
429
+ if status == "data_error_marker":
430
+ skipped_data_error_marker_count += 1
431
+ continue
432
+
433
+ save_path, oom_marker_path, data_error_path = rank_dataset.cache_paths(data_id)
434
+ if status == "load_error":
435
+ _write_json_atomic(data_error_path, data)
436
+ new_data_error_count += 1
437
+ tqdm.write(
438
+ f"Rank {accelerator.process_index}: data loading failed at local sample {data_id} "
439
+ f"({data['error_type']}: {data['error']}); recorded {data_error_path} and continuing."
440
+ )
441
+ continue
442
+
443
+ with accelerator.accumulate(model):
444
+ with torch.no_grad():
445
+ source = data.get("source") if isinstance(data, dict) else None
446
+ temp_save_path = f"{save_path}.tmp"
447
+ try:
448
+ processed_data = model(data)
449
+ if enable_model_cpu_offload:
450
+ offload_manager.after_backward()
451
+ torch.save(processed_data, temp_save_path)
452
+ os.replace(temp_save_path, save_path)
453
+ for stale_marker in (oom_marker_path, data_error_path):
454
+ if os.path.isfile(stale_marker):
455
+ os.unlink(stale_marker)
456
+ processed_count += 1
457
+ del processed_data
458
+ except RuntimeError as error:
459
+ is_cuda_oom = isinstance(error, torch.OutOfMemoryError) or "CUDA out of memory" in str(error)
460
+ if not skip_data_process_oom or not is_cuda_oom:
461
+ raise
462
+ error_message = str(error)
463
+ error.__traceback__ = None
464
+ if os.path.isfile(temp_save_path):
465
+ os.unlink(temp_save_path)
466
+ del data
467
+ gc.collect()
468
+ if torch.cuda.is_available():
469
+ torch.cuda.empty_cache()
470
+ marker = {
471
+ "rank": accelerator.process_index,
472
+ "local_data_id": data_id,
473
+ "global_data_id": accelerator.process_index + data_id * accelerator.num_processes,
474
+ "source": source,
475
+ "error": error_message,
476
+ "created_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
477
+ }
478
+ _write_json_atomic(oom_marker_path, marker)
479
+ new_oom_count += 1
480
+ tqdm.write(
481
+ f"Rank {accelerator.process_index}: CUDA OOM at local sample {data_id}; "
482
+ f"recorded {oom_marker_path} and continuing."
483
+ )
484
+ continue
485
+ progress_bar.set_postfix(
486
+ processed=processed_count,
487
+ cached=skipped_cache_count,
488
+ oom=new_oom_count,
489
+ data_error=new_data_error_count,
490
+ )
491
+ print(
492
+ f"Rank {accelerator.process_index} data processing complete: processed={processed_count}, "
493
+ f"cached={skipped_cache_count}, new_oom={new_oom_count}, "
494
+ f"cached_oom={skipped_oom_marker_count}, new_data_error={new_data_error_count}, "
495
+ f"cached_data_error={skipped_data_error_marker_count}."
496
+ )
497
+ accelerator.end_training()
498
+
499
+
500
+ def initialize_deepspeed_gradient_checkpointing(accelerator: Accelerator):
501
+ if getattr(accelerator.state, "deepspeed_plugin", None) is not None:
502
+ ds_config = accelerator.state.deepspeed_plugin.deepspeed_config
503
+ if "activation_checkpointing" in ds_config:
504
+ import deepspeed
505
+ act_config = ds_config["activation_checkpointing"]
506
+ deepspeed.checkpointing.configure(
507
+ mpu_=None,
508
+ partition_activations=act_config.get("partition_activations", False),
509
+ checkpoint_in_cpu=act_config.get("cpu_checkpointing", False),
510
+ contiguous_checkpointing=act_config.get("contiguous_memory_optimization", False)
511
+ )
512
+ else:
513
+ print("Do not find activation_checkpointing config in deepspeed config, skip initializing deepspeed gradient checkpointing.")
514
+
515
+
516
+ def report_peak_cuda_memory(accelerator: Accelerator, output_path: str):
517
+ if accelerator.device.type != "cuda":
518
+ return
519
+ torch.cuda.synchronize(accelerator.device)
520
+ local = torch.tensor(
521
+ [torch.cuda.max_memory_allocated(accelerator.device), torch.cuda.max_memory_reserved(accelerator.device)],
522
+ dtype=torch.int64,
523
+ device=accelerator.device,
524
+ )
525
+ gathered = accelerator.gather(local).reshape(-1, 2).cpu().tolist()
526
+ if accelerator.is_main_process:
527
+ mib = 1024 ** 2
528
+ ranks = [
529
+ {
530
+ "rank": rank,
531
+ "max_allocated_bytes": int(values[0]),
532
+ "max_reserved_bytes": int(values[1]),
533
+ "max_allocated_mib": round(values[0] / mib, 2),
534
+ "max_reserved_mib": round(values[1] / mib, 2),
535
+ }
536
+ for rank, values in enumerate(gathered)
537
+ ]
538
+ report = {
539
+ "world_size": len(ranks),
540
+ "per_rank": ranks,
541
+ "max_allocated_mib": max(item["max_allocated_mib"] for item in ranks),
542
+ "max_reserved_mib": max(item["max_reserved_mib"] for item in ranks),
543
+ "note": "PyTorch CUDA allocator peak; excludes non-PyTorch driver allocations.",
544
+ }
545
+ os.makedirs(output_path, exist_ok=True)
546
+ report_path = os.path.join(output_path, "peak_cuda_memory.json")
547
+ with open(report_path, "w", encoding="utf-8") as handle:
548
+ json.dump(report, handle, indent=2)
549
+ handle.write("\n")
550
+ print(f"Peak CUDA memory report saved to `{report_path}`: {report}")
551
+
552
+
553
+ @torch.no_grad()
554
+ def run_full_denoise_validation(accelerator, model, validation_dataset, model_logger, step, args):
555
+ """Run the same validation samples on every ZeRO rank.
556
+
557
+ ZeRO-3 forward passes contain parameter collectives, so every rank must execute
558
+ the same number and order of denoising calls. Each metric is reduced across
559
+ ranks to catch any numerical divergence, then averaged across validation items.
560
+ """
561
+ if len(validation_dataset) == 0:
562
+ raise ValueError("Validation cache manifest is empty")
563
+ accelerator.wait_for_everyone()
564
+ was_training = model.training
565
+ model.eval()
566
+ started_at = time.time()
567
+ totals = {}
568
+ sample_records = []
569
+ iterator = tqdm(
570
+ range(len(validation_dataset)),
571
+ desc=f"validation@{step}",
572
+ disable=not accelerator.is_main_process,
573
+ )
574
+ for sample_id in iterator:
575
+ data = validation_dataset[sample_id]
576
+ validation_result = model(
577
+ {},
578
+ inputs=data,
579
+ validation_config={
580
+ "num_inference_steps": args.validation_num_inference_steps,
581
+ "seed": args.validation_seed + sample_id,
582
+ "flow_shift": args.validation_flow_shift,
583
+ "audio_flow_shift": args.validation_audio_flow_shift,
584
+ "cfg_scale": args.validation_cfg_scale,
585
+ "save_outputs": args.validation_save_outputs,
586
+ },
587
+ )
588
+ if (
589
+ isinstance(validation_result, tuple)
590
+ and len(validation_result) == 2
591
+ and isinstance(validation_result[1], dict)
592
+ ):
593
+ metrics, outputs = validation_result
594
+ else:
595
+ metrics, outputs = validation_result, None
596
+ reduced_metrics = {}
597
+ for key, value in metrics.items():
598
+ reduced = accelerator.reduce(value.detach(), reduction="mean")
599
+ scalar = float(reduced.float().item())
600
+ totals[key] = totals.get(key, 0.0) + scalar
601
+ reduced_metrics[key] = scalar
602
+ sample_record = {"sample_id": sample_id, **reduced_metrics}
603
+ if accelerator.is_main_process and outputs is not None:
604
+ from diffsynth.utils.data.audio_video import write_video_audio
605
+ validation_dir = os.path.join(
606
+ model_logger.output_path, "validation", f"step-{step:09d}",
607
+ )
608
+ os.makedirs(validation_dir, exist_ok=True)
609
+ output_path = os.path.join(validation_dir, f"sample-{sample_id:02d}.mp4")
610
+ write_video_audio(
611
+ video=outputs["video"], audio=outputs.get("audio"), output_path=output_path,
612
+ fps=24, audio_sample_rate=outputs.get("audio_sample_rate"),
613
+ )
614
+ sample_record["output_path"] = output_path
615
+ print(f"Saved validation sample {sample_id} to `{output_path}`.")
616
+ if accelerator.is_main_process:
617
+ sample_records.append(sample_record)
618
+ del data, metrics, outputs, validation_result
619
+
620
+ averaged = {f"validation/{key}": value / len(validation_dataset) for key, value in totals.items()}
621
+ if "validation/latent_mse" in averaged:
622
+ # A canonical validation loss key for W&B dashboards and sweep tooling.
623
+ # The modality-specific MSE values remain available alongside it.
624
+ averaged["validation/loss"] = averaged["validation/latent_mse"]
625
+ elapsed = time.time() - started_at
626
+ averaged["validation/seconds"] = elapsed
627
+ model_logger.log_metrics(accelerator, averaged, step)
628
+ if accelerator.is_main_process:
629
+ os.makedirs(model_logger.output_path, exist_ok=True)
630
+ history_path = os.path.join(model_logger.output_path, "validation_history.jsonl")
631
+ record = {
632
+ "step": step,
633
+ "num_samples": len(validation_dataset),
634
+ "num_inference_steps": args.validation_num_inference_steps,
635
+ "cfg_scale": args.validation_cfg_scale,
636
+ "elapsed_seconds": elapsed,
637
+ "metrics": averaged,
638
+ "samples": sample_records,
639
+ }
640
+ with open(history_path, "a", encoding="utf-8") as handle:
641
+ handle.write(json.dumps(record, ensure_ascii=False) + "\n")
642
+ print(f"Validation step {step}: {averaged}. Details appended to `{history_path}`.")
643
+ if was_training:
644
+ model.train()
645
+ accelerator.wait_for_everyone()