Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- .github/workflows/logo.gif +3 -0
- .github/workflows/publish.yaml +29 -0
- .gitignore +178 -0
- LICENSE +201 -0
- README.md +0 -0
- README_zh.md +0 -0
- diffsynth/__init__.py +1 -0
- diffsynth/configs/__init__.py +2 -0
- diffsynth/configs/model_configs.py +0 -0
- diffsynth/configs/vram_management_module_maps.py +517 -0
- diffsynth/core/__init__.py +8 -0
- diffsynth/core/attention/__init__.py +1 -0
- diffsynth/core/attention/attention.py +255 -0
- diffsynth/core/data/__init__.py +1 -0
- diffsynth/core/data/operators.py +341 -0
- diffsynth/core/data/unified_dataset.py +140 -0
- diffsynth/core/device/__init__.py +2 -0
- diffsynth/core/device/npu_compatible_device.py +109 -0
- diffsynth/core/gradient/__init__.py +1 -0
- diffsynth/core/gradient/gradient_checkpoint.py +65 -0
- diffsynth/core/loader/__init__.py +3 -0
- diffsynth/core/loader/config.py +177 -0
- diffsynth/core/loader/file.py +151 -0
- diffsynth/core/loader/model.py +167 -0
- diffsynth/core/npu_patch/npu_fused_operator.py +30 -0
- diffsynth/core/offload_training/__init__.py +1 -0
- diffsynth/core/offload_training/manager.py +177 -0
- diffsynth/core/offload_training/memory_buffer.py +136 -0
- diffsynth/core/offload_training/offloader.py +71 -0
- diffsynth/core/quant/__init__.py +3 -0
- diffsynth/core/quant/backends/__init__.py +28 -0
- diffsynth/core/quant/backends/bitsandbytes.py +155 -0
- diffsynth/core/quant/backends/comfy_kitchen.py +318 -0
- diffsynth/core/quant/backends/torchao.py +162 -0
- diffsynth/core/quant/base.py +243 -0
- diffsynth/core/quant/config.py +570 -0
- diffsynth/core/vram/__init__.py +2 -0
- diffsynth/core/vram/disk_map.py +93 -0
- diffsynth/core/vram/initialization.py +21 -0
- diffsynth/core/vram/layers.py +620 -0
- diffsynth/diffusion/__init__.py +7 -0
- diffsynth/diffusion/base_pipeline.py +542 -0
- diffsynth/diffusion/ddim_scheduler.py +109 -0
- diffsynth/diffusion/dmd2.py +601 -0
- diffsynth/diffusion/flow_match.py +442 -0
- diffsynth/diffusion/logger.py +170 -0
- diffsynth/diffusion/loss.py +225 -0
- diffsynth/diffusion/parsers.py +140 -0
- 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
|
.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()
|