Instructions to use saik0s/comfy_backup with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use saik0s/comfy_backup with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="saik0s/comfy_backup", filename="models/text_encoders/Qwen3VL-8B-Uncensored-HauhauCS-Aggressive-Q8_0.gguf", )
llm.create_chat_completion( messages = "No input example has been defined for this model task." )
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use saik0s/comfy_backup with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf saik0s/comfy_backup:Q8_0 # Run inference directly in the terminal: llama cli -hf saik0s/comfy_backup:Q8_0
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf saik0s/comfy_backup:Q8_0 # Run inference directly in the terminal: llama cli -hf saik0s/comfy_backup:Q8_0
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf saik0s/comfy_backup:Q8_0 # Run inference directly in the terminal: ./llama-cli -hf saik0s/comfy_backup:Q8_0
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf saik0s/comfy_backup:Q8_0 # Run inference directly in the terminal: ./build/bin/llama-cli -hf saik0s/comfy_backup:Q8_0
Use Docker
docker model run hf.co/saik0s/comfy_backup:Q8_0
- LM Studio
- Jan
- Ollama
How to use saik0s/comfy_backup with Ollama:
ollama run hf.co/saik0s/comfy_backup:Q8_0
- Unsloth Studio
How to use saik0s/comfy_backup with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for saik0s/comfy_backup to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for saik0s/comfy_backup to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for saik0s/comfy_backup to start chatting
- Pi
How to use saik0s/comfy_backup with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf saik0s/comfy_backup:Q8_0
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "saik0s/comfy_backup:Q8_0" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent new
How to use saik0s/comfy_backup with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf saik0s/comfy_backup:Q8_0
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default saik0s/comfy_backup:Q8_0
Run Hermes
hermes
- Atomic Chat new
- OpenClaw new
How to use saik0s/comfy_backup with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf saik0s/comfy_backup:Q8_0
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "saik0s/comfy_backup:Q8_0" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
- Docker Model Runner
How to use saik0s/comfy_backup with Docker Model Runner:
docker model run hf.co/saik0s/comfy_backup:Q8_0
- Lemonade
How to use saik0s/comfy_backup with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull saik0s/comfy_backup:Q8_0
Run and chat with the model
lemonade run user.comfy_backup-Q8_0
List all available models
lemonade list
Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .github/ISSUE_TEMPLATE/bug-report.yml +58 -0
- .github/ISSUE_TEMPLATE/config.yml +11 -0
- .github/ISSUE_TEMPLATE/feature-request.yml +32 -0
- .github/ISSUE_TEMPLATE/user-support.yml +40 -0
- .github/PULL_REQUEST_TEMPLATE/api-node.md +21 -0
- .github/scripts/check-ai-co-authors.sh +103 -0
- .github/workflows/api-node-template.yml +58 -0
- .github/workflows/backport_release.yaml +519 -0
- .github/workflows/check-ai-co-authors.yml +19 -0
- .github/workflows/check-line-endings.yml +40 -0
- .github/workflows/detect-unreviewed-merge.yml +24 -0
- .github/workflows/openapi-lint.yml +31 -0
- .github/workflows/pullrequest-ci-run.yml +53 -0
- .github/workflows/release-stable-all.yml +78 -0
- .github/workflows/release-webhook.yml +144 -0
- .github/workflows/ruff.yml +48 -0
- .github/workflows/stable-release.yml +172 -0
- .github/workflows/stale-issues.yml +21 -0
- .github/workflows/tag-dispatch-cloud.yml +45 -0
- .github/workflows/test-build.yml +31 -0
- .github/workflows/test-ci.yml +99 -0
- .github/workflows/test-execution.yml +30 -0
- .github/workflows/test-launch.yml +47 -0
- .github/workflows/test-unit.yml +30 -0
- .github/workflows/update-api-stubs.yml +56 -0
- .github/workflows/update-ci-container.yml +59 -0
- .github/workflows/update-version.yml +59 -0
- .github/workflows/windows_release_dependencies.yml +72 -0
- .github/workflows/windows_release_dependencies_manual.yml +64 -0
- .github/workflows/windows_release_nightly_pytorch.yml +93 -0
- .github/workflows/windows_release_package.yml +106 -0
- comfy/audio_encoders/audio_encoders.py +92 -0
- comfy/audio_encoders/wav2vec2.py +252 -0
- comfy/audio_encoders/whisper.py +186 -0
- comfy/background_removal/birefnet.json +7 -0
- comfy/background_removal/birefnet.py +689 -0
- comfy/cldm/cldm.py +434 -0
- comfy/cldm/control_types.py +10 -0
- comfy/cldm/dit_embedder.py +120 -0
- comfy/cldm/mmdit.py +81 -0
- comfy/comfy_types/README.md +43 -0
- comfy/comfy_types/__init__.py +46 -0
- comfy/comfy_types/examples/example_nodes.py +28 -0
- comfy/comfy_types/node_typing.py +353 -0
- comfy/diffusers_load.py +36 -0
- comfy/extra_samplers/uni_pc.py +873 -0
- comfy/float.py +266 -0
- comfy/gligen.py +299 -0
- comfy/hooks.py +786 -0
- comfy/image_encoders/dino2.py +199 -0
.github/ISSUE_TEMPLATE/bug-report.yml
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Bug Report
|
| 2 |
+
description: "Something is broken inside of ComfyUI. (Do not use this if you're just having issues and need help, or if the issue relates to a custom node)"
|
| 3 |
+
labels: ["Potential Bug"]
|
| 4 |
+
body:
|
| 5 |
+
- type: markdown
|
| 6 |
+
attributes:
|
| 7 |
+
value: |
|
| 8 |
+
Before submitting a **Bug Report**, please ensure the following:
|
| 9 |
+
|
| 10 |
+
- **1:** You are running the latest version of ComfyUI.
|
| 11 |
+
- **2:** You have your ComfyUI logs and relevant workflow on hand and will post them in this bug report.
|
| 12 |
+
- **3:** You confirmed that the bug is not caused by a custom node. You can disable all custom nodes by passing
|
| 13 |
+
`--disable-all-custom-nodes` command line argument. If you have custom node try updating them to the latest version.
|
| 14 |
+
- **4:** This is an actual bug in ComfyUI, not just a support question. A bug is when you can specify exact
|
| 15 |
+
steps to replicate what went wrong and others will be able to repeat your steps and see the same issue happen.
|
| 16 |
+
|
| 17 |
+
## Very Important
|
| 18 |
+
|
| 19 |
+
Please make sure that you post ALL your ComfyUI logs in the bug report **even if there is no crash**. Just paste everything. The startup log (everything before "To see the GUI go to: ...") contains critical information to developers trying to help. For a performance issue or crash, paste everything from "got prompt" to the end, including the crash. More is better - always. A bug report without logs will likely be ignored.
|
| 20 |
+
- type: checkboxes
|
| 21 |
+
id: custom-nodes-test
|
| 22 |
+
attributes:
|
| 23 |
+
label: Custom Node Testing
|
| 24 |
+
description: Please confirm you have tried to reproduce the issue with all custom nodes disabled.
|
| 25 |
+
options:
|
| 26 |
+
- label: I have tried disabling custom nodes and the issue persists (see [how to disable custom nodes](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled) if you need help)
|
| 27 |
+
required: false
|
| 28 |
+
- type: textarea
|
| 29 |
+
attributes:
|
| 30 |
+
label: Expected Behavior
|
| 31 |
+
description: "What you expected to happen."
|
| 32 |
+
validations:
|
| 33 |
+
required: true
|
| 34 |
+
- type: textarea
|
| 35 |
+
attributes:
|
| 36 |
+
label: Actual Behavior
|
| 37 |
+
description: "What actually happened. Please include a screenshot of the issue if possible."
|
| 38 |
+
validations:
|
| 39 |
+
required: true
|
| 40 |
+
- type: textarea
|
| 41 |
+
attributes:
|
| 42 |
+
label: Steps to Reproduce
|
| 43 |
+
description: "Describe how to reproduce the issue. Please be sure to attach a workflow JSON or PNG, ideally one that doesn't require custom nodes to test. If the bug open happens when certain custom nodes are used, most likely that custom node is what has the bug rather than ComfyUI, in which case it should be reported to the node's author."
|
| 44 |
+
validations:
|
| 45 |
+
required: true
|
| 46 |
+
- type: textarea
|
| 47 |
+
attributes:
|
| 48 |
+
label: Debug Logs
|
| 49 |
+
description: "Please copy the output from your terminal logs here."
|
| 50 |
+
render: powershell
|
| 51 |
+
validations:
|
| 52 |
+
required: true
|
| 53 |
+
- type: textarea
|
| 54 |
+
attributes:
|
| 55 |
+
label: Other
|
| 56 |
+
description: "Any other additional information you think might be helpful."
|
| 57 |
+
validations:
|
| 58 |
+
required: false
|
.github/ISSUE_TEMPLATE/config.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
blank_issues_enabled: true
|
| 2 |
+
contact_links:
|
| 3 |
+
- name: ComfyUI Frontend Issues
|
| 4 |
+
url: https://github.com/Comfy-Org/ComfyUI_frontend/issues
|
| 5 |
+
about: Issues related to the ComfyUI frontend (display issues, user interaction bugs), please go to the frontend repo to file the issue
|
| 6 |
+
- name: ComfyUI Matrix Space
|
| 7 |
+
url: https://app.element.io/#/room/%23comfyui_space%3Amatrix.org
|
| 8 |
+
about: The ComfyUI Matrix Space is available for support and general discussion related to ComfyUI (Matrix is like Discord but open source).
|
| 9 |
+
- name: Comfy Org Discord
|
| 10 |
+
url: https://discord.gg/comfyorg
|
| 11 |
+
about: The Comfy Org Discord is available for support and general discussion related to ComfyUI.
|
.github/ISSUE_TEMPLATE/feature-request.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Feature Request
|
| 2 |
+
description: "You have an idea for something new you would like to see added to ComfyUI's core."
|
| 3 |
+
labels: [ "Feature" ]
|
| 4 |
+
body:
|
| 5 |
+
- type: markdown
|
| 6 |
+
attributes:
|
| 7 |
+
value: |
|
| 8 |
+
Before submitting a **Feature Request**, please ensure the following:
|
| 9 |
+
|
| 10 |
+
**1:** You are running the latest version of ComfyUI.
|
| 11 |
+
**2:** You have looked to make sure there is not already a feature that does what you need, and there is not already a Feature Request listed for the same idea.
|
| 12 |
+
**3:** This is something that makes sense to add to ComfyUI Core, and wouldn't make more sense as a custom node.
|
| 13 |
+
|
| 14 |
+
If unsure, ask on the [ComfyUI Matrix Space](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) or the [Comfy Org Discord](https://discord.gg/comfyorg) first.
|
| 15 |
+
- type: textarea
|
| 16 |
+
attributes:
|
| 17 |
+
label: Feature Idea
|
| 18 |
+
description: "Describe the feature you want to see."
|
| 19 |
+
validations:
|
| 20 |
+
required: true
|
| 21 |
+
- type: textarea
|
| 22 |
+
attributes:
|
| 23 |
+
label: Existing Solutions
|
| 24 |
+
description: "Please search through available custom nodes / extensions to see if there are existing custom solutions for this. If so, please link the options you found here as a reference."
|
| 25 |
+
validations:
|
| 26 |
+
required: false
|
| 27 |
+
- type: textarea
|
| 28 |
+
attributes:
|
| 29 |
+
label: Other
|
| 30 |
+
description: "Any other additional information you think might be helpful."
|
| 31 |
+
validations:
|
| 32 |
+
required: false
|
.github/ISSUE_TEMPLATE/user-support.yml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: User Support
|
| 2 |
+
description: "Use this if you need help with something, or you're experiencing an issue."
|
| 3 |
+
labels: [ "User Support" ]
|
| 4 |
+
body:
|
| 5 |
+
- type: markdown
|
| 6 |
+
attributes:
|
| 7 |
+
value: |
|
| 8 |
+
Before submitting a **User Report** issue, please ensure the following:
|
| 9 |
+
|
| 10 |
+
**1:** You are running the latest version of ComfyUI.
|
| 11 |
+
**2:** You have made an effort to find public answers to your question before asking here. In other words, you googled it first, and scrolled through recent help topics.
|
| 12 |
+
|
| 13 |
+
If unsure, ask on the [ComfyUI Matrix Space](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) or the [Comfy Org Discord](https://discord.gg/comfyorg) first.
|
| 14 |
+
- type: checkboxes
|
| 15 |
+
id: custom-nodes-test
|
| 16 |
+
attributes:
|
| 17 |
+
label: Custom Node Testing
|
| 18 |
+
description: Please confirm you have tried to reproduce the issue with all custom nodes disabled.
|
| 19 |
+
options:
|
| 20 |
+
- label: I have tried disabling custom nodes and the issue persists (see [how to disable custom nodes](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled) if you need help)
|
| 21 |
+
required: false
|
| 22 |
+
- type: textarea
|
| 23 |
+
attributes:
|
| 24 |
+
label: Your question
|
| 25 |
+
description: "Post your question here. Please be as detailed as possible."
|
| 26 |
+
validations:
|
| 27 |
+
required: true
|
| 28 |
+
- type: textarea
|
| 29 |
+
attributes:
|
| 30 |
+
label: Logs
|
| 31 |
+
description: "If your question relates to an issue you're experiencing, please go to `Server` -> `Logs` -> potentially set `View Type` to `Debug` as well, then copypaste all the text into here."
|
| 32 |
+
render: powershell
|
| 33 |
+
validations:
|
| 34 |
+
required: false
|
| 35 |
+
- type: textarea
|
| 36 |
+
attributes:
|
| 37 |
+
label: Other
|
| 38 |
+
description: "Any other additional information you think might be helpful."
|
| 39 |
+
validations:
|
| 40 |
+
required: false
|
.github/PULL_REQUEST_TEMPLATE/api-node.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- API_NODE_PR_CHECKLIST: do not remove -->
|
| 2 |
+
|
| 3 |
+
## API Node PR Checklist
|
| 4 |
+
|
| 5 |
+
### Scope
|
| 6 |
+
- [ ] **Is API Node Change**
|
| 7 |
+
|
| 8 |
+
### Pricing & Billing
|
| 9 |
+
- [ ] **Need pricing update**
|
| 10 |
+
- [ ] **No pricing update**
|
| 11 |
+
|
| 12 |
+
If **Need pricing update**:
|
| 13 |
+
- [ ] Metronome rate cards updated
|
| 14 |
+
- [ ] Auto‑billing tests updated and passing
|
| 15 |
+
|
| 16 |
+
### QA
|
| 17 |
+
- [ ] **QA done**
|
| 18 |
+
- [ ] **QA not required**
|
| 19 |
+
|
| 20 |
+
### Comms
|
| 21 |
+
- [ ] Informed **Kosinkadink**
|
.github/scripts/check-ai-co-authors.sh
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Checks pull request commits for AI agent Co-authored-by trailers.
|
| 3 |
+
# Exits non-zero when any are found and prints fix instructions.
|
| 4 |
+
set -euo pipefail
|
| 5 |
+
|
| 6 |
+
base_sha="${1:?usage: check-ai-co-authors.sh <base_sha> <head_sha>}"
|
| 7 |
+
head_sha="${2:?usage: check-ai-co-authors.sh <base_sha> <head_sha>}"
|
| 8 |
+
|
| 9 |
+
# Known AI coding-agent trailer patterns (case-insensitive).
|
| 10 |
+
# Each entry is an extended-regex fragment matched against Co-authored-by lines.
|
| 11 |
+
AGENT_PATTERNS=(
|
| 12 |
+
# Anthropic — Claude Code / Amp
|
| 13 |
+
'noreply@anthropic\.com'
|
| 14 |
+
# Cursor
|
| 15 |
+
'cursoragent@cursor\.com'
|
| 16 |
+
# GitHub Copilot
|
| 17 |
+
'copilot-swe-agent\[bot\]'
|
| 18 |
+
'copilot@github\.com'
|
| 19 |
+
# OpenAI Codex
|
| 20 |
+
'noreply@openai\.com'
|
| 21 |
+
'codex@openai\.com'
|
| 22 |
+
# Aider
|
| 23 |
+
'aider@aider\.chat'
|
| 24 |
+
# Google — Gemini / Jules
|
| 25 |
+
'gemini@google\.com'
|
| 26 |
+
'jules@google\.com'
|
| 27 |
+
# Windsurf / Codeium
|
| 28 |
+
'@codeium\.com'
|
| 29 |
+
# Devin
|
| 30 |
+
'devin-ai-integration\[bot\]'
|
| 31 |
+
'devin@cognition\.ai'
|
| 32 |
+
'devin@cognition-labs\.com'
|
| 33 |
+
# Amazon Q Developer
|
| 34 |
+
'amazon-q-developer'
|
| 35 |
+
'@amazon\.com.*[Qq].[Dd]eveloper'
|
| 36 |
+
# Cline
|
| 37 |
+
'cline-bot'
|
| 38 |
+
'cline@cline\.ai'
|
| 39 |
+
# Continue
|
| 40 |
+
'continue-agent'
|
| 41 |
+
'continue@continue\.dev'
|
| 42 |
+
# Sourcegraph
|
| 43 |
+
'noreply@sourcegraph\.com'
|
| 44 |
+
# Generic catch-alls for common agent name patterns
|
| 45 |
+
'Co-authored-by:.*\b[Cc]laude\b'
|
| 46 |
+
'Co-authored-by:.*\b[Cc]opilot\b'
|
| 47 |
+
'Co-authored-by:.*\b[Cc]ursor\b'
|
| 48 |
+
'Co-authored-by:.*\b[Cc]odex\b'
|
| 49 |
+
'Co-authored-by:.*\b[Gg]emini\b'
|
| 50 |
+
'Co-authored-by:.*\b[Aa]ider\b'
|
| 51 |
+
'Co-authored-by:.*\b[Dd]evin\b'
|
| 52 |
+
'Co-authored-by:.*\b[Ww]indsurf\b'
|
| 53 |
+
'Co-authored-by:.*\b[Cc]line\b'
|
| 54 |
+
'Co-authored-by:.*\b[Aa]mazon Q\b'
|
| 55 |
+
'Co-authored-by:.*\b[Jj]ules\b'
|
| 56 |
+
'Co-authored-by:.*\bOpenCode\b'
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Build a single alternation regex from all patterns.
|
| 60 |
+
regex=""
|
| 61 |
+
for pattern in "${AGENT_PATTERNS[@]}"; do
|
| 62 |
+
if [[ -n "$regex" ]]; then
|
| 63 |
+
regex="${regex}|${pattern}"
|
| 64 |
+
else
|
| 65 |
+
regex="$pattern"
|
| 66 |
+
fi
|
| 67 |
+
done
|
| 68 |
+
|
| 69 |
+
# Collect Co-authored-by lines from every commit in the PR range.
|
| 70 |
+
violations=""
|
| 71 |
+
while IFS= read -r sha; do
|
| 72 |
+
message="$(git log -1 --format='%B' "$sha")"
|
| 73 |
+
matched_lines="$(echo "$message" | grep -iE "^Co-authored-by:" || true)"
|
| 74 |
+
if [[ -z "$matched_lines" ]]; then
|
| 75 |
+
continue
|
| 76 |
+
fi
|
| 77 |
+
|
| 78 |
+
while IFS= read -r line; do
|
| 79 |
+
if echo "$line" | grep -iqE "$regex"; then
|
| 80 |
+
short="$(git log -1 --format='%h' "$sha")"
|
| 81 |
+
violations="${violations} ${short}: ${line}"$'\n'
|
| 82 |
+
fi
|
| 83 |
+
done <<< "$matched_lines"
|
| 84 |
+
done < <(git rev-list "${base_sha}..${head_sha}")
|
| 85 |
+
|
| 86 |
+
if [[ -n "$violations" ]]; then
|
| 87 |
+
echo "::error::AI agent Co-authored-by trailers detected in PR commits."
|
| 88 |
+
echo ""
|
| 89 |
+
echo "The following commits contain Co-authored-by trailers from AI coding agents:"
|
| 90 |
+
echo ""
|
| 91 |
+
echo "$violations"
|
| 92 |
+
echo "These trailers should be removed before merging."
|
| 93 |
+
echo ""
|
| 94 |
+
echo "To fix, rewrite the commit messages with:"
|
| 95 |
+
echo " git rebase -i ${base_sha}"
|
| 96 |
+
echo ""
|
| 97 |
+
echo "and remove the Co-authored-by lines, then force-push your branch."
|
| 98 |
+
echo ""
|
| 99 |
+
echo "If you believe this is a false positive, please open an issue."
|
| 100 |
+
exit 1
|
| 101 |
+
fi
|
| 102 |
+
|
| 103 |
+
echo "No AI agent Co-authored-by trailers found."
|
.github/workflows/api-node-template.yml
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Append API Node PR template
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request_target:
|
| 5 |
+
types: [opened, reopened, synchronize, ready_for_review]
|
| 6 |
+
paths:
|
| 7 |
+
- 'comfy_api_nodes/**' # only run if these files changed
|
| 8 |
+
|
| 9 |
+
permissions:
|
| 10 |
+
contents: read
|
| 11 |
+
pull-requests: write
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
inject:
|
| 15 |
+
runs-on: ubuntu-latest
|
| 16 |
+
steps:
|
| 17 |
+
- name: Ensure template exists and append to PR body
|
| 18 |
+
uses: actions/github-script@v7
|
| 19 |
+
with:
|
| 20 |
+
script: |
|
| 21 |
+
const { owner, repo } = context.repo;
|
| 22 |
+
const number = context.payload.pull_request.number;
|
| 23 |
+
const templatePath = '.github/PULL_REQUEST_TEMPLATE/api-node.md';
|
| 24 |
+
const marker = '<!-- API_NODE_PR_CHECKLIST: do not remove -->';
|
| 25 |
+
|
| 26 |
+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: number });
|
| 27 |
+
|
| 28 |
+
let templateText;
|
| 29 |
+
try {
|
| 30 |
+
const res = await github.rest.repos.getContent({
|
| 31 |
+
owner,
|
| 32 |
+
repo,
|
| 33 |
+
path: templatePath,
|
| 34 |
+
ref: pr.base.ref
|
| 35 |
+
});
|
| 36 |
+
const buf = Buffer.from(res.data.content, res.data.encoding || 'base64');
|
| 37 |
+
templateText = buf.toString('utf8');
|
| 38 |
+
} catch (e) {
|
| 39 |
+
core.setFailed(`Required PR template not found at "${templatePath}" on ${pr.base.ref}. Please add it to the repo.`);
|
| 40 |
+
return;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Enforce the presence of the marker inside the template (for idempotence)
|
| 44 |
+
if (!templateText.includes(marker)) {
|
| 45 |
+
core.setFailed(`Template at "${templatePath}" does not contain the required marker:\n${marker}\nAdd it so we can detect duplicates safely.`);
|
| 46 |
+
return;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// If the PR already contains the marker, do not append again.
|
| 50 |
+
const body = pr.body || '';
|
| 51 |
+
if (body.includes(marker)) {
|
| 52 |
+
core.info('Template already present in PR body; nothing to inject.');
|
| 53 |
+
return;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const newBody = (body ? body + '\n\n' : '') + templateText + '\n';
|
| 57 |
+
await github.rest.pulls.update({ owner, repo, pull_number: number, body: newBody });
|
| 58 |
+
core.notice('API Node template appended to PR description.');
|
.github/workflows/backport_release.yaml
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Backport Release
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
inputs:
|
| 6 |
+
commit:
|
| 7 |
+
description: 'Full 40-char SHA of the tip commit of the backport source branch (the PR head commit that passed tests). The branch is resolved from this SHA and must be unique.'
|
| 8 |
+
required: true
|
| 9 |
+
type: string
|
| 10 |
+
|
| 11 |
+
permissions:
|
| 12 |
+
contents: read
|
| 13 |
+
pull-requests: read
|
| 14 |
+
checks: read
|
| 15 |
+
|
| 16 |
+
jobs:
|
| 17 |
+
backport-release:
|
| 18 |
+
name: Create backport release
|
| 19 |
+
runs-on: ubuntu-latest
|
| 20 |
+
environment: backport release
|
| 21 |
+
|
| 22 |
+
steps:
|
| 23 |
+
- name: Generate GitHub App token
|
| 24 |
+
id: app-token
|
| 25 |
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
| 26 |
+
with:
|
| 27 |
+
app-id: ${{ secrets.FEN_RELEASE_APP_ID }}
|
| 28 |
+
private-key: ${{ secrets.FEN_RELEASE_PRIVATE_KEY }}
|
| 29 |
+
|
| 30 |
+
- name: Checkout repository
|
| 31 |
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
| 32 |
+
with:
|
| 33 |
+
token: ${{ steps.app-token.outputs.token }}
|
| 34 |
+
fetch-depth: 0
|
| 35 |
+
fetch-tags: true
|
| 36 |
+
|
| 37 |
+
- name: Configure git
|
| 38 |
+
run: |
|
| 39 |
+
git config user.name "fen-release[bot]"
|
| 40 |
+
git config user.email "fen-release[bot]@users.noreply.github.com"
|
| 41 |
+
|
| 42 |
+
- name: Resolve source branch from commit SHA
|
| 43 |
+
id: resolve
|
| 44 |
+
env:
|
| 45 |
+
SOURCE_COMMIT: ${{ inputs.commit }}
|
| 46 |
+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
| 47 |
+
run: |
|
| 48 |
+
set -euo pipefail
|
| 49 |
+
|
| 50 |
+
# Require a full 40-char lowercase-hex SHA. Short SHAs are ambiguous
|
| 51 |
+
# and we will be comparing this value against API responses (PR head
|
| 52 |
+
# SHA, ref tips) that always return the full form.
|
| 53 |
+
if [[ ! "${SOURCE_COMMIT}" =~ ^[0-9a-f]{40}$ ]]; then
|
| 54 |
+
echo "::error::Input commit '${SOURCE_COMMIT}' is not a full 40-char lowercase hex SHA."
|
| 55 |
+
exit 1
|
| 56 |
+
fi
|
| 57 |
+
|
| 58 |
+
# Fetch all remote branches so we can search for which one(s) point
|
| 59 |
+
# at this SHA. `actions/checkout` with fetch-depth: 0 fetches full
|
| 60 |
+
# history of the checked-out ref but does not necessarily populate
|
| 61 |
+
# every refs/remotes/origin/*, so do it explicitly.
|
| 62 |
+
git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*'
|
| 63 |
+
|
| 64 |
+
# Verify the commit actually exists in this repo's object DB.
|
| 65 |
+
if ! git cat-file -e "${SOURCE_COMMIT}^{commit}" 2>/dev/null; then
|
| 66 |
+
echo "::error::Commit ${SOURCE_COMMIT} was not found in the repository."
|
| 67 |
+
exit 1
|
| 68 |
+
fi
|
| 69 |
+
|
| 70 |
+
# Find every remote branch whose tip == SOURCE_COMMIT. Exactly one
|
| 71 |
+
# branch must point at it. If zero, the commit isn't anyone's tip
|
| 72 |
+
# (likely stale, force-pushed past, or never the PR head). If more
|
| 73 |
+
# than one, the (branch -> SHA) mapping is ambiguous and we refuse
|
| 74 |
+
# to guess — the operator must give us a unique branch to release.
|
| 75 |
+
mapfile -t matching_branches < <(
|
| 76 |
+
git for-each-ref \
|
| 77 |
+
--format='%(refname:strip=3)' \
|
| 78 |
+
--points-at="${SOURCE_COMMIT}" \
|
| 79 |
+
refs/remotes/origin/ \
|
| 80 |
+
| grep -vx 'HEAD' || true
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
if [[ "${#matching_branches[@]}" -eq 0 ]]; then
|
| 84 |
+
echo "::error::No branch on origin has ${SOURCE_COMMIT} as its tip."
|
| 85 |
+
echo "::error::Either the branch was updated after you copied this SHA, or this commit was never the head of a branch."
|
| 86 |
+
exit 1
|
| 87 |
+
fi
|
| 88 |
+
|
| 89 |
+
if [[ "${#matching_branches[@]}" -gt 1 ]]; then
|
| 90 |
+
echo "::error::More than one branch on origin has ${SOURCE_COMMIT} as its tip; cannot pick one:"
|
| 91 |
+
for b in "${matching_branches[@]}"; do
|
| 92 |
+
echo "::error:: - ${b}"
|
| 93 |
+
done
|
| 94 |
+
echo "::error::Refusing to proceed with an ambiguous source branch."
|
| 95 |
+
exit 1
|
| 96 |
+
fi
|
| 97 |
+
|
| 98 |
+
source_branch="${matching_branches[0]}"
|
| 99 |
+
|
| 100 |
+
if [[ "${source_branch}" == "${DEFAULT_BRANCH}" ]]; then
|
| 101 |
+
echo "::error::Source branch must not be the default branch ('${DEFAULT_BRANCH}')."
|
| 102 |
+
exit 1
|
| 103 |
+
fi
|
| 104 |
+
|
| 105 |
+
echo "Resolved commit ${SOURCE_COMMIT} to branch '${source_branch}'."
|
| 106 |
+
echo "source_branch=${source_branch}" >> "$GITHUB_OUTPUT"
|
| 107 |
+
|
| 108 |
+
- name: Determine latest stable release
|
| 109 |
+
id: latest
|
| 110 |
+
env:
|
| 111 |
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
| 112 |
+
run: |
|
| 113 |
+
set -euo pipefail
|
| 114 |
+
|
| 115 |
+
# List all tags matching vMAJOR.MINOR.PATCH and pick the highest by numeric
|
| 116 |
+
# comparison of each component. We DO NOT use `sort -V` because it treats
|
| 117 |
+
# v0.19.99 as higher than v0.20.1.
|
| 118 |
+
latest_tag="$(
|
| 119 |
+
git tag --list 'v[0-9]*.[0-9]*.[0-9]*' \
|
| 120 |
+
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
| 121 |
+
| awk -F'[v.]' '{ printf "%010d %010d %010d %s\n", $2, $3, $4, $0 }' \
|
| 122 |
+
| sort -k1,1n -k2,2n -k3,3n \
|
| 123 |
+
| tail -n1 \
|
| 124 |
+
| awk '{print $4}'
|
| 125 |
+
)"
|
| 126 |
+
|
| 127 |
+
if [[ -z "${latest_tag}" ]]; then
|
| 128 |
+
echo "::error::No stable release tags (vMAJOR.MINOR.PATCH) were found."
|
| 129 |
+
exit 1
|
| 130 |
+
fi
|
| 131 |
+
|
| 132 |
+
# Parse components
|
| 133 |
+
ver="${latest_tag#v}"
|
| 134 |
+
major="${ver%%.*}"
|
| 135 |
+
rest="${ver#*.}"
|
| 136 |
+
minor="${rest%%.*}"
|
| 137 |
+
patch="${rest#*.}"
|
| 138 |
+
|
| 139 |
+
new_patch=$((patch + 1))
|
| 140 |
+
new_version="v${major}.${minor}.${new_patch}"
|
| 141 |
+
release_branch="release/v${major}.${minor}"
|
| 142 |
+
|
| 143 |
+
latest_sha="$(git rev-list -n 1 "refs/tags/${latest_tag}")"
|
| 144 |
+
|
| 145 |
+
echo "latest_tag=${latest_tag}" >> "$GITHUB_OUTPUT"
|
| 146 |
+
echo "latest_sha=${latest_sha}" >> "$GITHUB_OUTPUT"
|
| 147 |
+
echo "major=${major}" >> "$GITHUB_OUTPUT"
|
| 148 |
+
echo "minor=${minor}" >> "$GITHUB_OUTPUT"
|
| 149 |
+
echo "patch=${patch}" >> "$GITHUB_OUTPUT"
|
| 150 |
+
echo "new_version=${new_version}" >> "$GITHUB_OUTPUT"
|
| 151 |
+
echo "new_version_no_v=${major}.${minor}.${new_patch}" >> "$GITHUB_OUTPUT"
|
| 152 |
+
echo "release_branch=${release_branch}" >> "$GITHUB_OUTPUT"
|
| 153 |
+
|
| 154 |
+
echo "Latest stable release: ${latest_tag} (${latest_sha})"
|
| 155 |
+
echo "New version will be: ${new_version}"
|
| 156 |
+
echo "Release branch: ${release_branch}"
|
| 157 |
+
|
| 158 |
+
- name: Validate source branch is cut directly from the latest stable release
|
| 159 |
+
env:
|
| 160 |
+
SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }}
|
| 161 |
+
SOURCE_COMMIT: ${{ inputs.commit }}
|
| 162 |
+
LATEST_TAG_SHA: ${{ steps.latest.outputs.latest_sha }}
|
| 163 |
+
LATEST_TAG: ${{ steps.latest.outputs.latest_tag }}
|
| 164 |
+
run: |
|
| 165 |
+
set -euo pipefail
|
| 166 |
+
|
| 167 |
+
# Use the user-provided SHA directly rather than re-resolving the branch
|
| 168 |
+
# tip — the resolve step already proved the branch tip equals SOURCE_COMMIT,
|
| 169 |
+
# and pinning to the SHA here makes the rest of the job TOCTOU-safe against
|
| 170 |
+
# someone pushing to the branch mid-run.
|
| 171 |
+
source_sha="${SOURCE_COMMIT}"
|
| 172 |
+
|
| 173 |
+
# Walking first-parent from the source tip must reach LATEST_TAG_SHA.
|
| 174 |
+
# We capture rev-list into a variable and grep against a here-string
|
| 175 |
+
# rather than piping `rev-list | grep -q`: under `set -o pipefail`,
|
| 176 |
+
# `grep -q` would exit on first match and SIGPIPE the still-streaming
|
| 177 |
+
# `rev-list`, propagating exit 141 as a spurious "not found".
|
| 178 |
+
first_parent_chain="$(git rev-list --first-parent "${source_sha}")"
|
| 179 |
+
if ! grep -Fxq "${LATEST_TAG_SHA}" <<< "${first_parent_chain}"; then
|
| 180 |
+
echo "::error::Source branch '${SOURCE_BRANCH}' is not cut from '${LATEST_TAG}'."
|
| 181 |
+
echo "::error::Its first-parent history does not include ${LATEST_TAG_SHA}."
|
| 182 |
+
exit 1
|
| 183 |
+
fi
|
| 184 |
+
|
| 185 |
+
# Additionally, every commit added on top of the tag (the set we are
|
| 186 |
+
# about to publish) must itself be a descendant of the tag along
|
| 187 |
+
# first-parent — i.e. no sibling commits from master sneak in via a
|
| 188 |
+
# non-first-parent path. Enforce by requiring that the symmetric
|
| 189 |
+
# difference is empty in one direction: commits in source that are
|
| 190 |
+
# NOT first-parent-reachable from source starting at the tag.
|
| 191 |
+
# We do this by intersecting:
|
| 192 |
+
# A = commits reachable from source but not from tag (full DAG)
|
| 193 |
+
# B = commits on the first-parent chain from source down to tag
|
| 194 |
+
# and requiring A == B.
|
| 195 |
+
all_added="$(git rev-list "${LATEST_TAG_SHA}..${source_sha}" | sort)"
|
| 196 |
+
first_parent_added="$(
|
| 197 |
+
git rev-list --first-parent "${LATEST_TAG_SHA}..${source_sha}" | sort
|
| 198 |
+
)"
|
| 199 |
+
|
| 200 |
+
if [[ "${all_added}" != "${first_parent_added}" ]]; then
|
| 201 |
+
echo "::error::Source branch '${SOURCE_BRANCH}' contains commits not on its first-parent chain from '${LATEST_TAG}'."
|
| 202 |
+
echo "::error::This usually means the branch was cut from master (not from the tag) or contains a merge from master."
|
| 203 |
+
echo "Commits reachable but not on first-parent chain:"
|
| 204 |
+
comm -23 <(printf '%s\n' "${all_added}") <(printf '%s\n' "${first_parent_added}") \
|
| 205 |
+
| while read -r sha; do
|
| 206 |
+
echo " $(git log -1 --format='%h %s' "${sha}")"
|
| 207 |
+
done
|
| 208 |
+
exit 1
|
| 209 |
+
fi
|
| 210 |
+
|
| 211 |
+
added_count="$(printf '%s\n' "${all_added}" | grep -c . || true)"
|
| 212 |
+
echo "Source branch is cut directly from ${LATEST_TAG} with ${added_count} commit(s) on top."
|
| 213 |
+
|
| 214 |
+
- name: Validate PR exists, is open, named correctly, has latest commit, and checks pass
|
| 215 |
+
env:
|
| 216 |
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
| 217 |
+
SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }}
|
| 218 |
+
SOURCE_COMMIT: ${{ inputs.commit }}
|
| 219 |
+
NEW_VERSION: ${{ steps.latest.outputs.new_version }}
|
| 220 |
+
REPO: ${{ github.repository }}
|
| 221 |
+
run: |
|
| 222 |
+
set -euo pipefail
|
| 223 |
+
|
| 224 |
+
expected_title="ComfyUI backport release ${NEW_VERSION}"
|
| 225 |
+
|
| 226 |
+
# Find open PRs from this branch into master. The --state open filter
|
| 227 |
+
# is load-bearing: a closed/merged PR with passing checks must not be
|
| 228 |
+
# accepted as authorization for a new release.
|
| 229 |
+
pr_json="$(
|
| 230 |
+
gh pr list \
|
| 231 |
+
--repo "${REPO}" \
|
| 232 |
+
--state open \
|
| 233 |
+
--head "${SOURCE_BRANCH}" \
|
| 234 |
+
--base master \
|
| 235 |
+
--json number,title,headRefOid,state \
|
| 236 |
+
--limit 10
|
| 237 |
+
)"
|
| 238 |
+
|
| 239 |
+
pr_count="$(echo "${pr_json}" | jq 'length')"
|
| 240 |
+
if [[ "${pr_count}" -eq 0 ]]; then
|
| 241 |
+
echo "::error::No open PR found from '${SOURCE_BRANCH}' into 'master'. The PR must exist and be open."
|
| 242 |
+
exit 1
|
| 243 |
+
fi
|
| 244 |
+
|
| 245 |
+
# Pick the PR matching the expected title
|
| 246 |
+
pr_number="$(echo "${pr_json}" | jq -r --arg t "${expected_title}" '
|
| 247 |
+
map(select(.title == $t)) | .[0].number // empty
|
| 248 |
+
')"
|
| 249 |
+
pr_head_sha="$(echo "${pr_json}" | jq -r --arg t "${expected_title}" '
|
| 250 |
+
map(select(.title == $t)) | .[0].headRefOid // empty
|
| 251 |
+
')"
|
| 252 |
+
|
| 253 |
+
if [[ -z "${pr_number}" ]]; then
|
| 254 |
+
echo "::error::No open PR from '${SOURCE_BRANCH}' into 'master' is titled '${expected_title}'."
|
| 255 |
+
echo "Found PRs:"
|
| 256 |
+
echo "${pr_json}" | jq -r '.[] | " #\(.number): \(.title)"'
|
| 257 |
+
exit 1
|
| 258 |
+
fi
|
| 259 |
+
|
| 260 |
+
# The PR's current head commit must equal the SHA the operator gave us.
|
| 261 |
+
# This is what closes the door on releasing stale code: if anyone has
|
| 262 |
+
# pushed to the branch since the operator validated tests passed, the
|
| 263 |
+
# PR head will have advanced past SOURCE_COMMIT and we abort. (The
|
| 264 |
+
# resolve step already proved the branch tip == SOURCE_COMMIT; this
|
| 265 |
+
# ties that same SHA to the PR that authorizes the release.)
|
| 266 |
+
if [[ "${pr_head_sha}" != "${SOURCE_COMMIT}" ]]; then
|
| 267 |
+
echo "::error::PR #${pr_number} head commit is ${pr_head_sha}, but the operator-provided commit is ${SOURCE_COMMIT}."
|
| 268 |
+
echo "::error::The PR has new commits since this release was authorized. Re-run with the new head SHA after verifying its checks."
|
| 269 |
+
exit 1
|
| 270 |
+
fi
|
| 271 |
+
|
| 272 |
+
echo "Found open PR #${pr_number} titled '${expected_title}' at head ${pr_head_sha} (matches operator-provided commit)."
|
| 273 |
+
|
| 274 |
+
# Verify all check runs on the head commit have completed successfully.
|
| 275 |
+
# A check is considered passing if conclusion is success, neutral, or skipped.
|
| 276 |
+
checks_json="$(
|
| 277 |
+
gh api \
|
| 278 |
+
--paginate \
|
| 279 |
+
"repos/${REPO}/commits/${pr_head_sha}/check-runs" \
|
| 280 |
+
--jq '.check_runs[] | {name: .name, status: .status, conclusion: .conclusion}'
|
| 281 |
+
)"
|
| 282 |
+
|
| 283 |
+
if [[ -z "${checks_json}" ]]; then
|
| 284 |
+
echo "::error::No check runs found on PR head commit ${pr_head_sha}."
|
| 285 |
+
exit 1
|
| 286 |
+
fi
|
| 287 |
+
|
| 288 |
+
echo "Check runs on ${pr_head_sha}:"
|
| 289 |
+
echo "${checks_json}" | jq -s '.'
|
| 290 |
+
|
| 291 |
+
failing="$(echo "${checks_json}" | jq -s '
|
| 292 |
+
map(select(
|
| 293 |
+
.status != "completed"
|
| 294 |
+
or (.conclusion as $c
|
| 295 |
+
| ["success","neutral","skipped"]
|
| 296 |
+
| index($c) | not)
|
| 297 |
+
))
|
| 298 |
+
')"
|
| 299 |
+
|
| 300 |
+
failing_count="$(echo "${failing}" | jq 'length')"
|
| 301 |
+
if [[ "${failing_count}" -gt 0 ]]; then
|
| 302 |
+
echo "::error::One or more checks have not passed on PR head commit ${pr_head_sha}:"
|
| 303 |
+
echo "${failing}" | jq -r '.[] | " - \(.name): status=\(.status) conclusion=\(.conclusion)"'
|
| 304 |
+
exit 1
|
| 305 |
+
fi
|
| 306 |
+
|
| 307 |
+
echo "All checks have passed on ${pr_head_sha}."
|
| 308 |
+
|
| 309 |
+
- name: Prepare release branch
|
| 310 |
+
id: prepare
|
| 311 |
+
env:
|
| 312 |
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
| 313 |
+
REPO: ${{ github.repository }}
|
| 314 |
+
RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }}
|
| 315 |
+
LATEST_TAG: ${{ steps.latest.outputs.latest_tag }}
|
| 316 |
+
LATEST_TAG_SHA: ${{ steps.latest.outputs.latest_sha }}
|
| 317 |
+
PATCH: ${{ steps.latest.outputs.patch }}
|
| 318 |
+
run: |
|
| 319 |
+
set -euo pipefail
|
| 320 |
+
|
| 321 |
+
# Try to fetch the release branch. If patch == 0, it shouldn't exist yet
|
| 322 |
+
# and we'll create it from the latest stable tag. If patch > 0, it must
|
| 323 |
+
# already exist and its tip must equal the latest stable tag commit (i.e.
|
| 324 |
+
# the previous patch release).
|
| 325 |
+
if git ls-remote --exit-code --heads origin "${RELEASE_BRANCH}" >/dev/null 2>&1; then
|
| 326 |
+
echo "Release branch '${RELEASE_BRANCH}' already exists on origin."
|
| 327 |
+
git fetch origin "refs/heads/${RELEASE_BRANCH}:refs/remotes/origin/${RELEASE_BRANCH}"
|
| 328 |
+
git checkout -B "${RELEASE_BRANCH}" "refs/remotes/origin/${RELEASE_BRANCH}"
|
| 329 |
+
|
| 330 |
+
current_tip="$(git rev-parse HEAD)"
|
| 331 |
+
if [[ "${current_tip}" != "${LATEST_TAG_SHA}" ]]; then
|
| 332 |
+
echo "::error::Release branch '${RELEASE_BRANCH}' tip (${current_tip}) is not at the latest stable release '${LATEST_TAG}' (${LATEST_TAG_SHA})."
|
| 333 |
+
echo "::error::Refusing to release on top of a divergent branch."
|
| 334 |
+
exit 1
|
| 335 |
+
fi
|
| 336 |
+
echo "branch_existed=true" >> "$GITHUB_OUTPUT"
|
| 337 |
+
else
|
| 338 |
+
if [[ "${PATCH}" != "0" ]]; then
|
| 339 |
+
echo "::error::Release branch '${RELEASE_BRANCH}' does not exist on origin, but the latest stable release '${LATEST_TAG}' has patch=${PATCH} (>0). This is inconsistent."
|
| 340 |
+
exit 1
|
| 341 |
+
fi
|
| 342 |
+
echo "Release branch '${RELEASE_BRANCH}' does not exist. Creating from ${LATEST_TAG}."
|
| 343 |
+
git checkout -B "${RELEASE_BRANCH}" "refs/tags/${LATEST_TAG}"
|
| 344 |
+
echo "branch_existed=false" >> "$GITHUB_OUTPUT"
|
| 345 |
+
fi
|
| 346 |
+
|
| 347 |
+
- name: Fast-forward merge source branch into release branch
|
| 348 |
+
env:
|
| 349 |
+
SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }}
|
| 350 |
+
SOURCE_COMMIT: ${{ inputs.commit }}
|
| 351 |
+
RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }}
|
| 352 |
+
run: |
|
| 353 |
+
set -euo pipefail
|
| 354 |
+
|
| 355 |
+
# --ff-only guarantees no merge commit is created. If a fast-forward is
|
| 356 |
+
# not possible (i.e. the release branch has commits the source branch
|
| 357 |
+
# doesn't), the merge will fail and we abort. Because we already validated
|
| 358 |
+
# that the source branch is rooted on the latest stable tag, and the
|
| 359 |
+
# release branch tip equals that same tag, this fast-forward should
|
| 360 |
+
# always succeed for a well-formed backport branch.
|
| 361 |
+
#
|
| 362 |
+
# We merge the operator-provided SHA, not the branch ref, so a push to
|
| 363 |
+
# the branch in the window between resolve and now cannot smuggle new
|
| 364 |
+
# commits into the release.
|
| 365 |
+
if ! git merge --ff-only "${SOURCE_COMMIT}"; then
|
| 366 |
+
echo "::error::Cannot fast-forward '${RELEASE_BRANCH}' to ${SOURCE_COMMIT} (tip of '${SOURCE_BRANCH}'). A merge commit would be required. Aborting."
|
| 367 |
+
exit 1
|
| 368 |
+
fi
|
| 369 |
+
|
| 370 |
+
echo "Fast-forwarded '${RELEASE_BRANCH}' to ${SOURCE_COMMIT} (tip of '${SOURCE_BRANCH}')."
|
| 371 |
+
|
| 372 |
+
- name: Bump version files
|
| 373 |
+
env:
|
| 374 |
+
NEW_VERSION_NO_V: ${{ steps.latest.outputs.new_version_no_v }}
|
| 375 |
+
run: |
|
| 376 |
+
set -euo pipefail
|
| 377 |
+
|
| 378 |
+
if [[ ! -f comfyui_version.py ]]; then
|
| 379 |
+
echo "::error::comfyui_version.py not found in repo root."
|
| 380 |
+
exit 1
|
| 381 |
+
fi
|
| 382 |
+
if [[ ! -f pyproject.toml ]]; then
|
| 383 |
+
echo "::error::pyproject.toml not found in repo root."
|
| 384 |
+
exit 1
|
| 385 |
+
fi
|
| 386 |
+
|
| 387 |
+
# Replace the version string in comfyui_version.py.
|
| 388 |
+
# Expected format: __version__ = "X.Y.Z"
|
| 389 |
+
python3 - "$NEW_VERSION_NO_V" <<'PY'
|
| 390 |
+
import re, sys, pathlib
|
| 391 |
+
new = sys.argv[1]
|
| 392 |
+
|
| 393 |
+
p = pathlib.Path("comfyui_version.py")
|
| 394 |
+
src = p.read_text()
|
| 395 |
+
new_src, n = re.subn(
|
| 396 |
+
r'(__version__\s*=\s*[\'"])[^\'"]+([\'"])',
|
| 397 |
+
lambda m: f'{m.group(1)}{new}{m.group(2)}',
|
| 398 |
+
src,
|
| 399 |
+
count=1,
|
| 400 |
+
)
|
| 401 |
+
if n != 1:
|
| 402 |
+
sys.exit("Could not find __version__ assignment in comfyui_version.py")
|
| 403 |
+
p.write_text(new_src)
|
| 404 |
+
|
| 405 |
+
p = pathlib.Path("pyproject.toml")
|
| 406 |
+
src = p.read_text()
|
| 407 |
+
# Replace the first `version = "..."` inside [project] or [tool.poetry].
|
| 408 |
+
new_src, n = re.subn(
|
| 409 |
+
r'(?m)^(version\s*=\s*")[^"]+(")',
|
| 410 |
+
lambda m: f'{m.group(1)}{new}{m.group(2)}',
|
| 411 |
+
src,
|
| 412 |
+
count=1,
|
| 413 |
+
)
|
| 414 |
+
if n != 1:
|
| 415 |
+
sys.exit("Could not find version assignment in pyproject.toml")
|
| 416 |
+
p.write_text(new_src)
|
| 417 |
+
PY
|
| 418 |
+
|
| 419 |
+
echo "Updated version to ${NEW_VERSION_NO_V} in comfyui_version.py and pyproject.toml."
|
| 420 |
+
git --no-pager diff -- comfyui_version.py pyproject.toml
|
| 421 |
+
|
| 422 |
+
- name: Commit version bump and tag release
|
| 423 |
+
env:
|
| 424 |
+
NEW_VERSION: ${{ steps.latest.outputs.new_version }}
|
| 425 |
+
run: |
|
| 426 |
+
set -euo pipefail
|
| 427 |
+
|
| 428 |
+
git add comfyui_version.py pyproject.toml
|
| 429 |
+
git commit -m "ComfyUI ${NEW_VERSION}"
|
| 430 |
+
|
| 431 |
+
if git rev-parse -q --verify "refs/tags/${NEW_VERSION}" >/dev/null; then
|
| 432 |
+
echo "::error::Tag ${NEW_VERSION} already exists locally."
|
| 433 |
+
exit 1
|
| 434 |
+
fi
|
| 435 |
+
git tag "${NEW_VERSION}"
|
| 436 |
+
|
| 437 |
+
- name: Verify tag does not already exist on origin
|
| 438 |
+
env:
|
| 439 |
+
NEW_VERSION: ${{ steps.latest.outputs.new_version }}
|
| 440 |
+
run: |
|
| 441 |
+
set -euo pipefail
|
| 442 |
+
if git ls-remote --exit-code --tags origin "refs/tags/${NEW_VERSION}" >/dev/null 2>&1; then
|
| 443 |
+
echo "::error::Tag ${NEW_VERSION} already exists on origin. Aborting."
|
| 444 |
+
exit 1
|
| 445 |
+
fi
|
| 446 |
+
|
| 447 |
+
- name: Push release branch and tag
|
| 448 |
+
env:
|
| 449 |
+
RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }}
|
| 450 |
+
NEW_VERSION: ${{ steps.latest.outputs.new_version }}
|
| 451 |
+
run: |
|
| 452 |
+
set -euo pipefail
|
| 453 |
+
|
| 454 |
+
# Push the branch first, then the tag. Atomic-ish: if the branch push
|
| 455 |
+
# fails we never publish the tag.
|
| 456 |
+
git push origin "refs/heads/${RELEASE_BRANCH}:refs/heads/${RELEASE_BRANCH}"
|
| 457 |
+
git push origin "refs/tags/${NEW_VERSION}"
|
| 458 |
+
|
| 459 |
+
echo "Released ${NEW_VERSION} on ${RELEASE_BRANCH}."
|
| 460 |
+
|
| 461 |
+
- name: Delete remote source branch
|
| 462 |
+
env:
|
| 463 |
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
| 464 |
+
REPO: ${{ github.repository }}
|
| 465 |
+
SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }}
|
| 466 |
+
SOURCE_COMMIT: ${{ inputs.commit }}
|
| 467 |
+
RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }}
|
| 468 |
+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
| 469 |
+
run: |
|
| 470 |
+
set -euo pipefail
|
| 471 |
+
|
| 472 |
+
# Belt-and-braces: the resolve step already refuses the default branch,
|
| 473 |
+
# but never delete the default or the release branch under any
|
| 474 |
+
# circumstances.
|
| 475 |
+
if [[ "${SOURCE_BRANCH}" == "${DEFAULT_BRANCH}" || "${SOURCE_BRANCH}" == "${RELEASE_BRANCH}" ]]; then
|
| 476 |
+
echo "::error::Refusing to delete '${SOURCE_BRANCH}' (matches default or release branch)."
|
| 477 |
+
exit 1
|
| 478 |
+
fi
|
| 479 |
+
|
| 480 |
+
# Delete the source branch on origin, but only if its tip is still the
|
| 481 |
+
# SHA we released from. If someone pushed new commits to it after we
|
| 482 |
+
# resolved it, leave it alone — those commits would be silently lost.
|
| 483 |
+
current_tip="$(git ls-remote origin "refs/heads/${SOURCE_BRANCH}" | awk '{print $1}')"
|
| 484 |
+
if [[ -z "${current_tip}" ]]; then
|
| 485 |
+
echo "Source branch '${SOURCE_BRANCH}' no longer exists on origin; nothing to delete."
|
| 486 |
+
exit 0
|
| 487 |
+
fi
|
| 488 |
+
if [[ "${current_tip}" != "${SOURCE_COMMIT}" ]]; then
|
| 489 |
+
echo "::warning::Source branch '${SOURCE_BRANCH}' tip (${current_tip}) no longer matches released commit (${SOURCE_COMMIT}). Leaving it in place."
|
| 490 |
+
exit 0
|
| 491 |
+
fi
|
| 492 |
+
|
| 493 |
+
git push origin --delete "refs/heads/${SOURCE_BRANCH}"
|
| 494 |
+
echo "Deleted remote branch '${SOURCE_BRANCH}'."
|
| 495 |
+
|
| 496 |
+
- name: Summary
|
| 497 |
+
if: always()
|
| 498 |
+
env:
|
| 499 |
+
NEW_VERSION: ${{ steps.latest.outputs.new_version }}
|
| 500 |
+
RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }}
|
| 501 |
+
LATEST_TAG: ${{ steps.latest.outputs.latest_tag }}
|
| 502 |
+
SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }}
|
| 503 |
+
SOURCE_COMMIT: ${{ inputs.commit }}
|
| 504 |
+
run: |
|
| 505 |
+
# SOURCE_BRANCH is empty if the resolve step never produced an output
|
| 506 |
+
# (e.g. the workflow failed in or before that step). Show a placeholder
|
| 507 |
+
# in that case so the summary table still renders cleanly.
|
| 508 |
+
source_branch_display="${SOURCE_BRANCH:-(unresolved)}"
|
| 509 |
+
{
|
| 510 |
+
echo "## Backport release"
|
| 511 |
+
echo ""
|
| 512 |
+
echo "| Field | Value |"
|
| 513 |
+
echo "|---|---|"
|
| 514 |
+
echo "| Source commit | \`${SOURCE_COMMIT}\` |"
|
| 515 |
+
echo "| Source branch | \`${source_branch_display}\` |"
|
| 516 |
+
echo "| Previous stable | \`${LATEST_TAG}\` |"
|
| 517 |
+
echo "| New version | \`${NEW_VERSION}\` |"
|
| 518 |
+
echo "| Release branch | \`${RELEASE_BRANCH}\` |"
|
| 519 |
+
} >> "$GITHUB_STEP_SUMMARY"
|
.github/workflows/check-ai-co-authors.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Check AI Co-Authors
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
branches: ['*']
|
| 6 |
+
|
| 7 |
+
jobs:
|
| 8 |
+
check-ai-co-authors:
|
| 9 |
+
name: Check for AI agent co-author trailers
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout code
|
| 14 |
+
uses: actions/checkout@v4
|
| 15 |
+
with:
|
| 16 |
+
fetch-depth: 0
|
| 17 |
+
|
| 18 |
+
- name: Check commits for AI co-author trailers
|
| 19 |
+
run: bash .github/scripts/check-ai-co-authors.sh "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}"
|
.github/workflows/check-line-endings.yml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Check for Windows Line Endings
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
branches: ['*'] # Trigger on all pull requests to any branch
|
| 6 |
+
|
| 7 |
+
jobs:
|
| 8 |
+
check-line-endings:
|
| 9 |
+
runs-on: ubuntu-latest
|
| 10 |
+
|
| 11 |
+
steps:
|
| 12 |
+
- name: Checkout code
|
| 13 |
+
uses: actions/checkout@v4
|
| 14 |
+
with:
|
| 15 |
+
fetch-depth: 0 # Fetch all history to compare changes
|
| 16 |
+
|
| 17 |
+
- name: Check for Windows line endings (CRLF)
|
| 18 |
+
run: |
|
| 19 |
+
# Get the list of changed files in the PR
|
| 20 |
+
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
|
| 21 |
+
|
| 22 |
+
# Flag to track if CRLF is found
|
| 23 |
+
CRLF_FOUND=false
|
| 24 |
+
|
| 25 |
+
# Loop through each changed file
|
| 26 |
+
for FILE in $CHANGED_FILES; do
|
| 27 |
+
# Check if the file exists and is a text file
|
| 28 |
+
if [ -f "$FILE" ] && file "$FILE" | grep -q "text"; then
|
| 29 |
+
# Check for CRLF line endings
|
| 30 |
+
if grep -UP '\r$' "$FILE"; then
|
| 31 |
+
echo "Error: Windows line endings (CRLF) detected in $FILE"
|
| 32 |
+
CRLF_FOUND=true
|
| 33 |
+
fi
|
| 34 |
+
fi
|
| 35 |
+
done
|
| 36 |
+
|
| 37 |
+
# Exit with error if CRLF was found
|
| 38 |
+
if [ "$CRLF_FOUND" = true ]; then
|
| 39 |
+
exit 1
|
| 40 |
+
fi
|
.github/workflows/detect-unreviewed-merge.yml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Detect Unreviewed Merge
|
| 2 |
+
|
| 3 |
+
# SOC 2 compliance — reusable workflow lives in Comfy-Org/github-workflows,
|
| 4 |
+
# tracking issues are filed in Comfy-Org/unreviewed-merges.
|
| 5 |
+
|
| 6 |
+
on:
|
| 7 |
+
push:
|
| 8 |
+
branches: [master]
|
| 9 |
+
|
| 10 |
+
concurrency:
|
| 11 |
+
group: detect-unreviewed-merge-${{ github.sha }}
|
| 12 |
+
cancel-in-progress: false
|
| 13 |
+
|
| 14 |
+
permissions:
|
| 15 |
+
contents: read
|
| 16 |
+
pull-requests: read
|
| 17 |
+
|
| 18 |
+
jobs:
|
| 19 |
+
detect:
|
| 20 |
+
uses: Comfy-Org/github-workflows/.github/workflows/detect-unreviewed-merge.yml@4d9cb6b87f953bb7cd69954280e1465fb9bd2040 # v1
|
| 21 |
+
with:
|
| 22 |
+
approval-mode: latest-per-reviewer
|
| 23 |
+
secrets:
|
| 24 |
+
UNREVIEWED_MERGES_TOKEN: ${{ secrets.UNREVIEWED_MERGES_TOKEN }}
|
.github/workflows/openapi-lint.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: OpenAPI Lint
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
paths:
|
| 6 |
+
- 'openapi.yaml'
|
| 7 |
+
- '.spectral.yaml'
|
| 8 |
+
- '.github/workflows/openapi-lint.yml'
|
| 9 |
+
|
| 10 |
+
permissions:
|
| 11 |
+
contents: read
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
spectral:
|
| 15 |
+
name: Run Spectral
|
| 16 |
+
runs-on: ubuntu-latest
|
| 17 |
+
|
| 18 |
+
steps:
|
| 19 |
+
- name: Checkout repository
|
| 20 |
+
uses: actions/checkout@v4
|
| 21 |
+
|
| 22 |
+
- name: Set up Node.js
|
| 23 |
+
uses: actions/setup-node@v4
|
| 24 |
+
with:
|
| 25 |
+
node-version: '20'
|
| 26 |
+
|
| 27 |
+
- name: Install Spectral
|
| 28 |
+
run: npm install -g @stoplight/spectral-cli@6
|
| 29 |
+
|
| 30 |
+
- name: Lint openapi.yaml
|
| 31 |
+
run: spectral lint openapi.yaml --ruleset .spectral.yaml --fail-severity=error
|
.github/workflows/pullrequest-ci-run.yml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This is the GitHub Workflow that drives full-GPU-enabled tests of pull requests to ComfyUI, when the 'Run-CI-Test' label is added
|
| 2 |
+
# Results are reported as checkmarks on the commits, as well as onto https://ci.comfy.org/
|
| 3 |
+
name: Pull Request CI Workflow Runs
|
| 4 |
+
on:
|
| 5 |
+
pull_request_target:
|
| 6 |
+
types: [labeled]
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
pr-test-stable:
|
| 10 |
+
if: ${{ github.event.label.name == 'Run-CI-Test' }}
|
| 11 |
+
strategy:
|
| 12 |
+
fail-fast: false
|
| 13 |
+
matrix:
|
| 14 |
+
os: [macos, linux, windows]
|
| 15 |
+
python_version: ["3.9", "3.10", "3.11", "3.12"]
|
| 16 |
+
cuda_version: ["12.1"]
|
| 17 |
+
torch_version: ["stable"]
|
| 18 |
+
include:
|
| 19 |
+
- os: macos
|
| 20 |
+
runner_label: [self-hosted, macOS]
|
| 21 |
+
flags: "--use-pytorch-cross-attention"
|
| 22 |
+
- os: linux
|
| 23 |
+
runner_label: [self-hosted, Linux]
|
| 24 |
+
flags: ""
|
| 25 |
+
- os: windows
|
| 26 |
+
runner_label: [self-hosted, Windows]
|
| 27 |
+
flags: ""
|
| 28 |
+
runs-on: ${{ matrix.runner_label }}
|
| 29 |
+
steps:
|
| 30 |
+
- name: Test Workflows
|
| 31 |
+
uses: comfy-org/comfy-action@main
|
| 32 |
+
with:
|
| 33 |
+
os: ${{ matrix.os }}
|
| 34 |
+
python_version: ${{ matrix.python_version }}
|
| 35 |
+
torch_version: ${{ matrix.torch_version }}
|
| 36 |
+
google_credentials: ${{ secrets.GCS_SERVICE_ACCOUNT_JSON }}
|
| 37 |
+
comfyui_flags: ${{ matrix.flags }}
|
| 38 |
+
use_prior_commit: 'true'
|
| 39 |
+
comment:
|
| 40 |
+
if: ${{ github.event.label.name == 'Run-CI-Test' }}
|
| 41 |
+
runs-on: ubuntu-latest
|
| 42 |
+
permissions:
|
| 43 |
+
pull-requests: write
|
| 44 |
+
steps:
|
| 45 |
+
- uses: actions/github-script@v6
|
| 46 |
+
with:
|
| 47 |
+
script: |
|
| 48 |
+
github.rest.issues.createComment({
|
| 49 |
+
issue_number: context.issue.number,
|
| 50 |
+
owner: context.repo.owner,
|
| 51 |
+
repo: context.repo.repo,
|
| 52 |
+
body: '(Automated Bot Message) CI Tests are running, you can view the results at https://ci.comfy.org/?branch=${{ github.event.pull_request.number }}%2Fmerge'
|
| 53 |
+
})
|
.github/workflows/release-stable-all.yml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Release Stable All Portable Versions"
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
inputs:
|
| 6 |
+
git_tag:
|
| 7 |
+
description: 'Git tag'
|
| 8 |
+
required: true
|
| 9 |
+
type: string
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
release_nvidia_default:
|
| 13 |
+
permissions:
|
| 14 |
+
contents: "write"
|
| 15 |
+
packages: "write"
|
| 16 |
+
pull-requests: "read"
|
| 17 |
+
name: "Release NVIDIA Default (cu130)"
|
| 18 |
+
uses: ./.github/workflows/stable-release.yml
|
| 19 |
+
with:
|
| 20 |
+
git_tag: ${{ inputs.git_tag }}
|
| 21 |
+
cache_tag: "cu130"
|
| 22 |
+
python_minor: "13"
|
| 23 |
+
python_patch: "12"
|
| 24 |
+
rel_name: "nvidia"
|
| 25 |
+
rel_extra_name: ""
|
| 26 |
+
test_release: true
|
| 27 |
+
secrets: inherit
|
| 28 |
+
|
| 29 |
+
release_nvidia_cu126:
|
| 30 |
+
permissions:
|
| 31 |
+
contents: "write"
|
| 32 |
+
packages: "write"
|
| 33 |
+
pull-requests: "read"
|
| 34 |
+
name: "Release NVIDIA cu126"
|
| 35 |
+
uses: ./.github/workflows/stable-release.yml
|
| 36 |
+
with:
|
| 37 |
+
git_tag: ${{ inputs.git_tag }}
|
| 38 |
+
cache_tag: "cu126"
|
| 39 |
+
python_minor: "12"
|
| 40 |
+
python_patch: "10"
|
| 41 |
+
rel_name: "nvidia"
|
| 42 |
+
rel_extra_name: "_cu126"
|
| 43 |
+
test_release: true
|
| 44 |
+
secrets: inherit
|
| 45 |
+
|
| 46 |
+
release_amd_rocm:
|
| 47 |
+
permissions:
|
| 48 |
+
contents: "write"
|
| 49 |
+
packages: "write"
|
| 50 |
+
pull-requests: "read"
|
| 51 |
+
name: "Release AMD ROCm 7.2"
|
| 52 |
+
uses: ./.github/workflows/stable-release.yml
|
| 53 |
+
with:
|
| 54 |
+
git_tag: ${{ inputs.git_tag }}
|
| 55 |
+
cache_tag: "rocm72"
|
| 56 |
+
python_minor: "12"
|
| 57 |
+
python_patch: "10"
|
| 58 |
+
rel_name: "amd"
|
| 59 |
+
rel_extra_name: ""
|
| 60 |
+
test_release: false
|
| 61 |
+
secrets: inherit
|
| 62 |
+
|
| 63 |
+
release_xpu:
|
| 64 |
+
permissions:
|
| 65 |
+
contents: "write"
|
| 66 |
+
packages: "write"
|
| 67 |
+
pull-requests: "read"
|
| 68 |
+
name: "Release Intel XPU"
|
| 69 |
+
uses: ./.github/workflows/stable-release.yml
|
| 70 |
+
with:
|
| 71 |
+
git_tag: ${{ inputs.git_tag }}
|
| 72 |
+
cache_tag: "xpu"
|
| 73 |
+
python_minor: "13"
|
| 74 |
+
python_patch: "12"
|
| 75 |
+
rel_name: "intel"
|
| 76 |
+
rel_extra_name: ""
|
| 77 |
+
test_release: true
|
| 78 |
+
secrets: inherit
|
.github/workflows/release-webhook.yml
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Release Webhook
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
release:
|
| 5 |
+
types: [published]
|
| 6 |
+
|
| 7 |
+
jobs:
|
| 8 |
+
send-webhook:
|
| 9 |
+
runs-on: ubuntu-latest
|
| 10 |
+
env:
|
| 11 |
+
DESKTOP_REPO_DISPATCH_TOKEN: ${{ secrets.DESKTOP_REPO_DISPATCH_TOKEN }}
|
| 12 |
+
steps:
|
| 13 |
+
- name: Send release webhook
|
| 14 |
+
env:
|
| 15 |
+
WEBHOOK_URL: ${{ secrets.RELEASE_GITHUB_WEBHOOK_URL }}
|
| 16 |
+
WEBHOOK_SECRET: ${{ secrets.RELEASE_GITHUB_WEBHOOK_SECRET }}
|
| 17 |
+
run: |
|
| 18 |
+
# Generate UUID for delivery ID
|
| 19 |
+
DELIVERY_ID=$(uuidgen)
|
| 20 |
+
HOOK_ID="release-webhook-$(date +%s)"
|
| 21 |
+
|
| 22 |
+
# Create webhook payload matching GitHub release webhook format
|
| 23 |
+
PAYLOAD=$(cat <<EOF
|
| 24 |
+
{
|
| 25 |
+
"action": "published",
|
| 26 |
+
"release": {
|
| 27 |
+
"id": ${{ github.event.release.id }},
|
| 28 |
+
"node_id": "${{ github.event.release.node_id }}",
|
| 29 |
+
"url": "${{ github.event.release.url }}",
|
| 30 |
+
"html_url": "${{ github.event.release.html_url }}",
|
| 31 |
+
"assets_url": "${{ github.event.release.assets_url }}",
|
| 32 |
+
"upload_url": "${{ github.event.release.upload_url }}",
|
| 33 |
+
"tag_name": "${{ github.event.release.tag_name }}",
|
| 34 |
+
"target_commitish": "${{ github.event.release.target_commitish }}",
|
| 35 |
+
"name": ${{ toJSON(github.event.release.name) }},
|
| 36 |
+
"body": ${{ toJSON(github.event.release.body) }},
|
| 37 |
+
"draft": ${{ github.event.release.draft }},
|
| 38 |
+
"prerelease": ${{ github.event.release.prerelease }},
|
| 39 |
+
"created_at": "${{ github.event.release.created_at }}",
|
| 40 |
+
"published_at": "${{ github.event.release.published_at }}",
|
| 41 |
+
"author": {
|
| 42 |
+
"login": "${{ github.event.release.author.login }}",
|
| 43 |
+
"id": ${{ github.event.release.author.id }},
|
| 44 |
+
"node_id": "${{ github.event.release.author.node_id }}",
|
| 45 |
+
"avatar_url": "${{ github.event.release.author.avatar_url }}",
|
| 46 |
+
"url": "${{ github.event.release.author.url }}",
|
| 47 |
+
"html_url": "${{ github.event.release.author.html_url }}",
|
| 48 |
+
"type": "${{ github.event.release.author.type }}",
|
| 49 |
+
"site_admin": ${{ github.event.release.author.site_admin }}
|
| 50 |
+
},
|
| 51 |
+
"tarball_url": "${{ github.event.release.tarball_url }}",
|
| 52 |
+
"zipball_url": "${{ github.event.release.zipball_url }}",
|
| 53 |
+
"assets": ${{ toJSON(github.event.release.assets) }}
|
| 54 |
+
},
|
| 55 |
+
"repository": {
|
| 56 |
+
"id": ${{ github.event.repository.id }},
|
| 57 |
+
"node_id": "${{ github.event.repository.node_id }}",
|
| 58 |
+
"name": "${{ github.event.repository.name }}",
|
| 59 |
+
"full_name": "${{ github.event.repository.full_name }}",
|
| 60 |
+
"private": ${{ github.event.repository.private }},
|
| 61 |
+
"owner": {
|
| 62 |
+
"login": "${{ github.event.repository.owner.login }}",
|
| 63 |
+
"id": ${{ github.event.repository.owner.id }},
|
| 64 |
+
"node_id": "${{ github.event.repository.owner.node_id }}",
|
| 65 |
+
"avatar_url": "${{ github.event.repository.owner.avatar_url }}",
|
| 66 |
+
"url": "${{ github.event.repository.owner.url }}",
|
| 67 |
+
"html_url": "${{ github.event.repository.owner.html_url }}",
|
| 68 |
+
"type": "${{ github.event.repository.owner.type }}",
|
| 69 |
+
"site_admin": ${{ github.event.repository.owner.site_admin }}
|
| 70 |
+
},
|
| 71 |
+
"html_url": "${{ github.event.repository.html_url }}",
|
| 72 |
+
"clone_url": "${{ github.event.repository.clone_url }}",
|
| 73 |
+
"git_url": "${{ github.event.repository.git_url }}",
|
| 74 |
+
"ssh_url": "${{ github.event.repository.ssh_url }}",
|
| 75 |
+
"url": "${{ github.event.repository.url }}",
|
| 76 |
+
"created_at": "${{ github.event.repository.created_at }}",
|
| 77 |
+
"updated_at": "${{ github.event.repository.updated_at }}",
|
| 78 |
+
"pushed_at": "${{ github.event.repository.pushed_at }}",
|
| 79 |
+
"default_branch": "${{ github.event.repository.default_branch }}",
|
| 80 |
+
"fork": ${{ github.event.repository.fork }}
|
| 81 |
+
},
|
| 82 |
+
"sender": {
|
| 83 |
+
"login": "${{ github.event.sender.login }}",
|
| 84 |
+
"id": ${{ github.event.sender.id }},
|
| 85 |
+
"node_id": "${{ github.event.sender.node_id }}",
|
| 86 |
+
"avatar_url": "${{ github.event.sender.avatar_url }}",
|
| 87 |
+
"url": "${{ github.event.sender.url }}",
|
| 88 |
+
"html_url": "${{ github.event.sender.html_url }}",
|
| 89 |
+
"type": "${{ github.event.sender.type }}",
|
| 90 |
+
"site_admin": ${{ github.event.sender.site_admin }}
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
EOF
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Generate HMAC-SHA256 signature
|
| 97 |
+
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | cut -d' ' -f2)
|
| 98 |
+
|
| 99 |
+
# Send webhook with required headers
|
| 100 |
+
curl -X POST "$WEBHOOK_URL" \
|
| 101 |
+
-H "Content-Type: application/json" \
|
| 102 |
+
-H "X-GitHub-Event: release" \
|
| 103 |
+
-H "X-GitHub-Delivery: $DELIVERY_ID" \
|
| 104 |
+
-H "X-GitHub-Hook-ID: $HOOK_ID" \
|
| 105 |
+
-H "X-Hub-Signature-256: sha256=$SIGNATURE" \
|
| 106 |
+
-H "User-Agent: GitHub-Actions-Webhook/1.0" \
|
| 107 |
+
-d "$PAYLOAD" \
|
| 108 |
+
--fail --silent --show-error
|
| 109 |
+
|
| 110 |
+
echo "✅ Release webhook sent successfully"
|
| 111 |
+
|
| 112 |
+
- name: Send repository dispatch to desktop
|
| 113 |
+
env:
|
| 114 |
+
DISPATCH_TOKEN: ${{ env.DESKTOP_REPO_DISPATCH_TOKEN }}
|
| 115 |
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
| 116 |
+
RELEASE_URL: ${{ github.event.release.html_url }}
|
| 117 |
+
run: |
|
| 118 |
+
set -euo pipefail
|
| 119 |
+
|
| 120 |
+
if [ -z "${DISPATCH_TOKEN:-}" ]; then
|
| 121 |
+
echo "::error::DESKTOP_REPO_DISPATCH_TOKEN is required but not set."
|
| 122 |
+
exit 1
|
| 123 |
+
fi
|
| 124 |
+
|
| 125 |
+
PAYLOAD="$(jq -n \
|
| 126 |
+
--arg release_tag "$RELEASE_TAG" \
|
| 127 |
+
--arg release_url "$RELEASE_URL" \
|
| 128 |
+
'{
|
| 129 |
+
event_type: "comfyui_release_published",
|
| 130 |
+
client_payload: {
|
| 131 |
+
release_tag: $release_tag,
|
| 132 |
+
release_url: $release_url
|
| 133 |
+
}
|
| 134 |
+
}')"
|
| 135 |
+
|
| 136 |
+
curl -fsSL \
|
| 137 |
+
-X POST \
|
| 138 |
+
-H "Accept: application/vnd.github+json" \
|
| 139 |
+
-H "Content-Type: application/json" \
|
| 140 |
+
-H "Authorization: Bearer ${DISPATCH_TOKEN}" \
|
| 141 |
+
https://api.github.com/repos/Comfy-Org/desktop/dispatches \
|
| 142 |
+
-d "$PAYLOAD"
|
| 143 |
+
|
| 144 |
+
echo "✅ Dispatched ComfyUI release ${RELEASE_TAG} to Comfy-Org/desktop"
|
.github/workflows/ruff.yml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Python Linting
|
| 2 |
+
|
| 3 |
+
on: [push, pull_request]
|
| 4 |
+
|
| 5 |
+
jobs:
|
| 6 |
+
ruff:
|
| 7 |
+
name: Run Ruff
|
| 8 |
+
runs-on: ubuntu-latest
|
| 9 |
+
|
| 10 |
+
steps:
|
| 11 |
+
- name: Checkout repository
|
| 12 |
+
uses: actions/checkout@v4
|
| 13 |
+
|
| 14 |
+
- name: Set up Python
|
| 15 |
+
uses: actions/setup-python@v2
|
| 16 |
+
with:
|
| 17 |
+
python-version: 3.x
|
| 18 |
+
|
| 19 |
+
- name: Install Ruff
|
| 20 |
+
run: pip install ruff
|
| 21 |
+
|
| 22 |
+
- name: Run Ruff
|
| 23 |
+
run: ruff check .
|
| 24 |
+
|
| 25 |
+
pylint:
|
| 26 |
+
name: Run Pylint
|
| 27 |
+
runs-on: ubuntu-latest
|
| 28 |
+
|
| 29 |
+
steps:
|
| 30 |
+
- name: Checkout repository
|
| 31 |
+
uses: actions/checkout@v4
|
| 32 |
+
|
| 33 |
+
- name: Set up Python
|
| 34 |
+
uses: actions/setup-python@v4
|
| 35 |
+
with:
|
| 36 |
+
python-version: '3.12'
|
| 37 |
+
|
| 38 |
+
- name: Install requirements
|
| 39 |
+
run: |
|
| 40 |
+
python -m pip install --upgrade pip
|
| 41 |
+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
| 42 |
+
pip install -r requirements.txt
|
| 43 |
+
|
| 44 |
+
- name: Install Pylint
|
| 45 |
+
run: pip install pylint
|
| 46 |
+
|
| 47 |
+
- name: Run Pylint
|
| 48 |
+
run: pylint comfy_api_nodes
|
.github/workflows/stable-release.yml
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
name: "Release Stable Version"
|
| 3 |
+
|
| 4 |
+
on:
|
| 5 |
+
workflow_call:
|
| 6 |
+
inputs:
|
| 7 |
+
git_tag:
|
| 8 |
+
description: 'Git tag'
|
| 9 |
+
required: true
|
| 10 |
+
type: string
|
| 11 |
+
cache_tag:
|
| 12 |
+
description: 'Cached dependencies tag'
|
| 13 |
+
required: true
|
| 14 |
+
type: string
|
| 15 |
+
default: "cu129"
|
| 16 |
+
python_minor:
|
| 17 |
+
description: 'Python minor version'
|
| 18 |
+
required: true
|
| 19 |
+
type: string
|
| 20 |
+
default: "13"
|
| 21 |
+
python_patch:
|
| 22 |
+
description: 'Python patch version'
|
| 23 |
+
required: true
|
| 24 |
+
type: string
|
| 25 |
+
default: "6"
|
| 26 |
+
rel_name:
|
| 27 |
+
description: 'Release name'
|
| 28 |
+
required: true
|
| 29 |
+
type: string
|
| 30 |
+
default: "nvidia"
|
| 31 |
+
rel_extra_name:
|
| 32 |
+
description: 'Release extra name'
|
| 33 |
+
required: false
|
| 34 |
+
type: string
|
| 35 |
+
default: ""
|
| 36 |
+
test_release:
|
| 37 |
+
description: 'Test Release'
|
| 38 |
+
required: true
|
| 39 |
+
type: boolean
|
| 40 |
+
default: true
|
| 41 |
+
workflow_dispatch:
|
| 42 |
+
inputs:
|
| 43 |
+
git_tag:
|
| 44 |
+
description: 'Git tag'
|
| 45 |
+
required: true
|
| 46 |
+
type: string
|
| 47 |
+
cache_tag:
|
| 48 |
+
description: 'Cached dependencies tag'
|
| 49 |
+
required: true
|
| 50 |
+
type: string
|
| 51 |
+
default: "cu129"
|
| 52 |
+
python_minor:
|
| 53 |
+
description: 'Python minor version'
|
| 54 |
+
required: true
|
| 55 |
+
type: string
|
| 56 |
+
default: "13"
|
| 57 |
+
python_patch:
|
| 58 |
+
description: 'Python patch version'
|
| 59 |
+
required: true
|
| 60 |
+
type: string
|
| 61 |
+
default: "6"
|
| 62 |
+
rel_name:
|
| 63 |
+
description: 'Release name'
|
| 64 |
+
required: true
|
| 65 |
+
type: string
|
| 66 |
+
default: "nvidia"
|
| 67 |
+
rel_extra_name:
|
| 68 |
+
description: 'Release extra name'
|
| 69 |
+
required: false
|
| 70 |
+
type: string
|
| 71 |
+
default: ""
|
| 72 |
+
test_release:
|
| 73 |
+
description: 'Test Release'
|
| 74 |
+
required: true
|
| 75 |
+
type: boolean
|
| 76 |
+
default: true
|
| 77 |
+
|
| 78 |
+
jobs:
|
| 79 |
+
package_comfy_windows:
|
| 80 |
+
permissions:
|
| 81 |
+
contents: "write"
|
| 82 |
+
packages: "write"
|
| 83 |
+
pull-requests: "read"
|
| 84 |
+
runs-on: windows-latest
|
| 85 |
+
steps:
|
| 86 |
+
- uses: actions/checkout@v4
|
| 87 |
+
with:
|
| 88 |
+
ref: ${{ inputs.git_tag }}
|
| 89 |
+
fetch-depth: 150
|
| 90 |
+
persist-credentials: false
|
| 91 |
+
- uses: actions/cache/restore@v4
|
| 92 |
+
id: cache
|
| 93 |
+
with:
|
| 94 |
+
path: |
|
| 95 |
+
${{ inputs.cache_tag }}_python_deps.tar
|
| 96 |
+
update_comfyui_and_python_dependencies.bat
|
| 97 |
+
key: ${{ runner.os }}-build-${{ inputs.cache_tag }}-${{ inputs.python_minor }}
|
| 98 |
+
- shell: bash
|
| 99 |
+
run: |
|
| 100 |
+
mv ${{ inputs.cache_tag }}_python_deps.tar ../
|
| 101 |
+
mv update_comfyui_and_python_dependencies.bat ../
|
| 102 |
+
cd ..
|
| 103 |
+
tar xf ${{ inputs.cache_tag }}_python_deps.tar
|
| 104 |
+
pwd
|
| 105 |
+
ls
|
| 106 |
+
|
| 107 |
+
- shell: bash
|
| 108 |
+
run: |
|
| 109 |
+
cd ..
|
| 110 |
+
cp -r ComfyUI ComfyUI_copy
|
| 111 |
+
curl https://www.python.org/ftp/python/3.${{ inputs.python_minor }}.${{ inputs.python_patch }}/python-3.${{ inputs.python_minor }}.${{ inputs.python_patch }}-embed-amd64.zip -o python_embeded.zip
|
| 112 |
+
unzip python_embeded.zip -d python_embeded
|
| 113 |
+
cd python_embeded
|
| 114 |
+
echo ${{ env.MINOR_VERSION }}
|
| 115 |
+
echo 'import site' >> ./python3${{ inputs.python_minor }}._pth
|
| 116 |
+
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
| 117 |
+
./python.exe get-pip.py
|
| 118 |
+
./python.exe -s -m pip install ../${{ inputs.cache_tag }}_python_deps/*
|
| 119 |
+
|
| 120 |
+
grep comfy ../ComfyUI/requirements.txt > ./requirements_comfyui.txt
|
| 121 |
+
./python.exe -s -m pip install -r requirements_comfyui.txt
|
| 122 |
+
rm requirements_comfyui.txt
|
| 123 |
+
|
| 124 |
+
sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth
|
| 125 |
+
|
| 126 |
+
if test -f ./Lib/site-packages/torch/lib/dnnl.lib; then
|
| 127 |
+
rm ./Lib/site-packages/torch/lib/dnnl.lib #I don't think this is actually used and I need the space
|
| 128 |
+
rm ./Lib/site-packages/torch/lib/libprotoc.lib
|
| 129 |
+
rm ./Lib/site-packages/torch/lib/libprotobuf.lib
|
| 130 |
+
fi
|
| 131 |
+
|
| 132 |
+
cd ..
|
| 133 |
+
|
| 134 |
+
git clone --depth 1 https://github.com/comfyanonymous/taesd
|
| 135 |
+
cp taesd/*.safetensors ./ComfyUI_copy/models/vae_approx/
|
| 136 |
+
|
| 137 |
+
mkdir ComfyUI_windows_portable
|
| 138 |
+
mv python_embeded ComfyUI_windows_portable
|
| 139 |
+
mv ComfyUI_copy ComfyUI_windows_portable/ComfyUI
|
| 140 |
+
|
| 141 |
+
cd ComfyUI_windows_portable
|
| 142 |
+
|
| 143 |
+
mkdir update
|
| 144 |
+
cp -r ComfyUI/.ci/update_windows/* ./update/
|
| 145 |
+
cp -r ComfyUI/.ci/windows_${{ inputs.rel_name }}_base_files/* ./
|
| 146 |
+
cp ../update_comfyui_and_python_dependencies.bat ./update/
|
| 147 |
+
|
| 148 |
+
echo 'local-portable' > ComfyUI/.comfy_environment
|
| 149 |
+
|
| 150 |
+
cd ..
|
| 151 |
+
|
| 152 |
+
"C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=768m -ms=on -mf=BCJ2 ComfyUI_windows_portable.7z ComfyUI_windows_portable
|
| 153 |
+
mv ComfyUI_windows_portable.7z ComfyUI/ComfyUI_windows_portable_${{ inputs.rel_name }}${{ inputs.rel_extra_name }}.7z
|
| 154 |
+
|
| 155 |
+
- shell: bash
|
| 156 |
+
if: ${{ inputs.test_release }}
|
| 157 |
+
run: |
|
| 158 |
+
cd ..
|
| 159 |
+
cd ComfyUI_windows_portable
|
| 160 |
+
python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu
|
| 161 |
+
|
| 162 |
+
python_embeded/python.exe -s ./update/update.py ComfyUI/
|
| 163 |
+
|
| 164 |
+
ls
|
| 165 |
+
|
| 166 |
+
- name: Upload binaries to release
|
| 167 |
+
uses: softprops/action-gh-release@v2
|
| 168 |
+
with:
|
| 169 |
+
files: ComfyUI_windows_portable_${{ inputs.rel_name }}${{ inputs.rel_extra_name }}.7z
|
| 170 |
+
tag_name: ${{ inputs.git_tag }}
|
| 171 |
+
draft: true
|
| 172 |
+
overwrite_files: true
|
.github/workflows/stale-issues.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 'Close stale issues'
|
| 2 |
+
on:
|
| 3 |
+
schedule:
|
| 4 |
+
# Run daily at 430 am PT
|
| 5 |
+
- cron: '30 11 * * *'
|
| 6 |
+
permissions:
|
| 7 |
+
issues: write
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
stale:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/stale@v9
|
| 14 |
+
with:
|
| 15 |
+
stale-issue-message: "This issue is being marked stale because it has not had any activity for 30 days. Reply below within 7 days if your issue still isn't solved, and it will be left open. Otherwise, the issue will be closed automatically."
|
| 16 |
+
days-before-stale: 30
|
| 17 |
+
days-before-close: 7
|
| 18 |
+
stale-issue-label: 'Stale'
|
| 19 |
+
only-labels: 'User Support'
|
| 20 |
+
exempt-all-assignees: true
|
| 21 |
+
exempt-all-milestones: true
|
.github/workflows/tag-dispatch-cloud.yml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Tag Dispatch to Cloud
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
tags:
|
| 6 |
+
- 'v*'
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
dispatch-cloud:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
steps:
|
| 12 |
+
- name: Send repository dispatch to cloud
|
| 13 |
+
env:
|
| 14 |
+
DISPATCH_TOKEN: ${{ secrets.CLOUD_REPO_DISPATCH_TOKEN }}
|
| 15 |
+
RELEASE_TAG: ${{ github.ref_name }}
|
| 16 |
+
run: |
|
| 17 |
+
set -euo pipefail
|
| 18 |
+
|
| 19 |
+
if [ -z "${DISPATCH_TOKEN:-}" ]; then
|
| 20 |
+
echo "::error::CLOUD_REPO_DISPATCH_TOKEN is required but not set."
|
| 21 |
+
exit 1
|
| 22 |
+
fi
|
| 23 |
+
|
| 24 |
+
RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/${RELEASE_TAG}"
|
| 25 |
+
|
| 26 |
+
PAYLOAD="$(jq -n \
|
| 27 |
+
--arg release_tag "$RELEASE_TAG" \
|
| 28 |
+
--arg release_url "$RELEASE_URL" \
|
| 29 |
+
'{
|
| 30 |
+
event_type: "comfyui_tag_pushed",
|
| 31 |
+
client_payload: {
|
| 32 |
+
release_tag: $release_tag,
|
| 33 |
+
release_url: $release_url
|
| 34 |
+
}
|
| 35 |
+
}')"
|
| 36 |
+
|
| 37 |
+
curl -fsSL \
|
| 38 |
+
-X POST \
|
| 39 |
+
-H "Accept: application/vnd.github+json" \
|
| 40 |
+
-H "Content-Type: application/json" \
|
| 41 |
+
-H "Authorization: Bearer ${DISPATCH_TOKEN}" \
|
| 42 |
+
https://api.github.com/repos/Comfy-Org/cloud/dispatches \
|
| 43 |
+
-d "$PAYLOAD"
|
| 44 |
+
|
| 45 |
+
echo "✅ Dispatched ComfyUI tag ${RELEASE_TAG} to Comfy-Org/cloud"
|
.github/workflows/test-build.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Build package
|
| 2 |
+
|
| 3 |
+
#
|
| 4 |
+
# This workflow is a test of the python package build.
|
| 5 |
+
# Install Python dependencies across different Python versions.
|
| 6 |
+
#
|
| 7 |
+
|
| 8 |
+
on:
|
| 9 |
+
push:
|
| 10 |
+
paths:
|
| 11 |
+
- "requirements.txt"
|
| 12 |
+
- ".github/workflows/test-build.yml"
|
| 13 |
+
|
| 14 |
+
jobs:
|
| 15 |
+
build:
|
| 16 |
+
name: Build Test
|
| 17 |
+
runs-on: ubuntu-latest
|
| 18 |
+
strategy:
|
| 19 |
+
fail-fast: false
|
| 20 |
+
matrix:
|
| 21 |
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
| 22 |
+
steps:
|
| 23 |
+
- uses: actions/checkout@v4
|
| 24 |
+
- name: Set up Python ${{ matrix.python-version }}
|
| 25 |
+
uses: actions/setup-python@v4
|
| 26 |
+
with:
|
| 27 |
+
python-version: ${{ matrix.python-version }}
|
| 28 |
+
- name: Install dependencies
|
| 29 |
+
run: |
|
| 30 |
+
python -m pip install --upgrade pip
|
| 31 |
+
pip install -r requirements.txt
|
.github/workflows/test-ci.yml
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This is the GitHub Workflow that drives automatic full-GPU-enabled tests of all new commits to the master branch of ComfyUI
|
| 2 |
+
# Results are reported as checkmarks on the commits, as well as onto https://ci.comfy.org/
|
| 3 |
+
name: Full Comfy CI Workflow Runs
|
| 4 |
+
on:
|
| 5 |
+
push:
|
| 6 |
+
branches:
|
| 7 |
+
- master
|
| 8 |
+
- release/**
|
| 9 |
+
paths-ignore:
|
| 10 |
+
- 'app/**'
|
| 11 |
+
- 'input/**'
|
| 12 |
+
- 'output/**'
|
| 13 |
+
- 'notebooks/**'
|
| 14 |
+
- 'script_examples/**'
|
| 15 |
+
- '.github/**'
|
| 16 |
+
- 'web/**'
|
| 17 |
+
workflow_dispatch:
|
| 18 |
+
|
| 19 |
+
jobs:
|
| 20 |
+
test-stable:
|
| 21 |
+
strategy:
|
| 22 |
+
fail-fast: false
|
| 23 |
+
matrix:
|
| 24 |
+
# os: [macos, linux, windows]
|
| 25 |
+
# os: [macos, linux]
|
| 26 |
+
os: [linux]
|
| 27 |
+
python_version: ["3.10", "3.11", "3.12"]
|
| 28 |
+
cuda_version: ["12.1"]
|
| 29 |
+
torch_version: ["stable"]
|
| 30 |
+
include:
|
| 31 |
+
# - os: macos
|
| 32 |
+
# runner_label: [self-hosted, macOS]
|
| 33 |
+
# flags: "--use-pytorch-cross-attention"
|
| 34 |
+
- os: linux
|
| 35 |
+
runner_label: [self-hosted, Linux]
|
| 36 |
+
flags: ""
|
| 37 |
+
# - os: windows
|
| 38 |
+
# runner_label: [self-hosted, Windows]
|
| 39 |
+
# flags: ""
|
| 40 |
+
runs-on: ${{ matrix.runner_label }}
|
| 41 |
+
steps:
|
| 42 |
+
- name: Test Workflows
|
| 43 |
+
uses: comfy-org/comfy-action@main
|
| 44 |
+
with:
|
| 45 |
+
os: ${{ matrix.os }}
|
| 46 |
+
python_version: ${{ matrix.python_version }}
|
| 47 |
+
torch_version: ${{ matrix.torch_version }}
|
| 48 |
+
google_credentials: ${{ secrets.GCS_SERVICE_ACCOUNT_JSON }}
|
| 49 |
+
comfyui_flags: ${{ matrix.flags }}
|
| 50 |
+
|
| 51 |
+
# test-win-nightly:
|
| 52 |
+
# strategy:
|
| 53 |
+
# fail-fast: true
|
| 54 |
+
# matrix:
|
| 55 |
+
# os: [windows]
|
| 56 |
+
# python_version: ["3.9", "3.10", "3.11", "3.12"]
|
| 57 |
+
# cuda_version: ["12.1"]
|
| 58 |
+
# torch_version: ["nightly"]
|
| 59 |
+
# include:
|
| 60 |
+
# - os: windows
|
| 61 |
+
# runner_label: [self-hosted, Windows]
|
| 62 |
+
# flags: ""
|
| 63 |
+
# runs-on: ${{ matrix.runner_label }}
|
| 64 |
+
# steps:
|
| 65 |
+
# - name: Test Workflows
|
| 66 |
+
# uses: comfy-org/comfy-action@main
|
| 67 |
+
# with:
|
| 68 |
+
# os: ${{ matrix.os }}
|
| 69 |
+
# python_version: ${{ matrix.python_version }}
|
| 70 |
+
# torch_version: ${{ matrix.torch_version }}
|
| 71 |
+
# google_credentials: ${{ secrets.GCS_SERVICE_ACCOUNT_JSON }}
|
| 72 |
+
# comfyui_flags: ${{ matrix.flags }}
|
| 73 |
+
|
| 74 |
+
test-unix-nightly:
|
| 75 |
+
strategy:
|
| 76 |
+
fail-fast: false
|
| 77 |
+
matrix:
|
| 78 |
+
# os: [macos, linux]
|
| 79 |
+
os: [linux]
|
| 80 |
+
python_version: ["3.11"]
|
| 81 |
+
cuda_version: ["12.1"]
|
| 82 |
+
torch_version: ["nightly"]
|
| 83 |
+
include:
|
| 84 |
+
# - os: macos
|
| 85 |
+
# runner_label: [self-hosted, macOS]
|
| 86 |
+
# flags: "--use-pytorch-cross-attention"
|
| 87 |
+
- os: linux
|
| 88 |
+
runner_label: [self-hosted, Linux]
|
| 89 |
+
flags: ""
|
| 90 |
+
runs-on: ${{ matrix.runner_label }}
|
| 91 |
+
steps:
|
| 92 |
+
- name: Test Workflows
|
| 93 |
+
uses: comfy-org/comfy-action@main
|
| 94 |
+
with:
|
| 95 |
+
os: ${{ matrix.os }}
|
| 96 |
+
python_version: ${{ matrix.python_version }}
|
| 97 |
+
torch_version: ${{ matrix.torch_version }}
|
| 98 |
+
google_credentials: ${{ secrets.GCS_SERVICE_ACCOUNT_JSON }}
|
| 99 |
+
comfyui_flags: ${{ matrix.flags }}
|
.github/workflows/test-execution.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Execution Tests
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [ main, master, release/** ]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [ main, master, release/** ]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
test:
|
| 11 |
+
strategy:
|
| 12 |
+
matrix:
|
| 13 |
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
| 14 |
+
runs-on: ${{ matrix.os }}
|
| 15 |
+
continue-on-error: true
|
| 16 |
+
steps:
|
| 17 |
+
- uses: actions/checkout@v4
|
| 18 |
+
- name: Set up Python
|
| 19 |
+
uses: actions/setup-python@v4
|
| 20 |
+
with:
|
| 21 |
+
python-version: '3.12'
|
| 22 |
+
- name: Install requirements
|
| 23 |
+
run: |
|
| 24 |
+
python -m pip install --upgrade pip
|
| 25 |
+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
pip install -r tests-unit/requirements.txt
|
| 28 |
+
- name: Run Execution Tests
|
| 29 |
+
run: |
|
| 30 |
+
python -m pytest tests/execution -v --skip-timing-checks
|
.github/workflows/test-launch.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Test server launches without errors
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [ main, master, release/** ]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [ main, master, release/** ]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
test:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout ComfyUI
|
| 14 |
+
uses: actions/checkout@v4
|
| 15 |
+
with:
|
| 16 |
+
repository: "Comfy-Org/ComfyUI"
|
| 17 |
+
path: "ComfyUI"
|
| 18 |
+
- uses: actions/setup-python@v4
|
| 19 |
+
with:
|
| 20 |
+
python-version: '3.10'
|
| 21 |
+
- name: Install requirements
|
| 22 |
+
run: |
|
| 23 |
+
python -m pip install --upgrade pip
|
| 24 |
+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
| 25 |
+
pip install -r requirements.txt
|
| 26 |
+
pip install wait-for-it
|
| 27 |
+
working-directory: ComfyUI
|
| 28 |
+
- name: Start ComfyUI server
|
| 29 |
+
run: |
|
| 30 |
+
python main.py --cpu 2>&1 | tee console_output.log &
|
| 31 |
+
wait-for-it --service 127.0.0.1:8188 -t 30
|
| 32 |
+
working-directory: ComfyUI
|
| 33 |
+
- name: Check for unhandled exceptions in server log
|
| 34 |
+
run: |
|
| 35 |
+
grep -v "Found comfy_kitchen backend triton: {'available': False, 'disabled': True, 'unavailable_reason': \"ImportError: No module named 'triton'\", 'capabilities': \[\]}" console_output.log | grep -v "Found comfy_kitchen backend triton: {'available': False, 'disabled': False, 'unavailable_reason': \"ImportError: No module named 'triton'\", 'capabilities': \[\]}" > console_output_filtered.log
|
| 36 |
+
cat console_output_filtered.log
|
| 37 |
+
if grep -qE "Exception|Error" console_output_filtered.log; then
|
| 38 |
+
echo "Unhandled exception/error found in server log."
|
| 39 |
+
exit 1
|
| 40 |
+
fi
|
| 41 |
+
working-directory: ComfyUI
|
| 42 |
+
- uses: actions/upload-artifact@v4
|
| 43 |
+
if: always()
|
| 44 |
+
with:
|
| 45 |
+
name: console-output
|
| 46 |
+
path: ComfyUI/console_output.log
|
| 47 |
+
retention-days: 30
|
.github/workflows/test-unit.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Unit Tests
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [ main, master, release/** ]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [ main, master, release/** ]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
test:
|
| 11 |
+
strategy:
|
| 12 |
+
matrix:
|
| 13 |
+
os: [ubuntu-latest, windows-2022, macos-latest]
|
| 14 |
+
runs-on: ${{ matrix.os }}
|
| 15 |
+
continue-on-error: true
|
| 16 |
+
steps:
|
| 17 |
+
- uses: actions/checkout@v4
|
| 18 |
+
- name: Set up Python
|
| 19 |
+
uses: actions/setup-python@v4
|
| 20 |
+
with:
|
| 21 |
+
python-version: '3.12'
|
| 22 |
+
- name: Install requirements
|
| 23 |
+
run: |
|
| 24 |
+
python -m pip install --upgrade pip
|
| 25 |
+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
- name: Run Unit Tests
|
| 28 |
+
run: |
|
| 29 |
+
pip install -r tests-unit/requirements.txt
|
| 30 |
+
python -m pytest tests-unit
|
.github/workflows/update-api-stubs.yml
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Generate Pydantic Stubs from api.comfy.org
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
schedule:
|
| 5 |
+
- cron: '0 0 * * 1'
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
generate-models:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout repository
|
| 14 |
+
uses: actions/checkout@v4
|
| 15 |
+
|
| 16 |
+
- name: Set up Python
|
| 17 |
+
uses: actions/setup-python@v4
|
| 18 |
+
with:
|
| 19 |
+
python-version: '3.10'
|
| 20 |
+
|
| 21 |
+
- name: Install dependencies
|
| 22 |
+
run: |
|
| 23 |
+
python -m pip install --upgrade pip
|
| 24 |
+
pip install 'datamodel-code-generator[http]'
|
| 25 |
+
npm install @redocly/cli
|
| 26 |
+
|
| 27 |
+
- name: Download OpenAPI spec
|
| 28 |
+
run: |
|
| 29 |
+
curl -o openapi.yaml https://api.comfy.org/openapi
|
| 30 |
+
|
| 31 |
+
- name: Filter OpenAPI spec with Redocly
|
| 32 |
+
run: |
|
| 33 |
+
npx @redocly/cli bundle openapi.yaml --output filtered-openapi.yaml --config comfy_api_nodes/redocly.yaml --remove-unused-components
|
| 34 |
+
|
| 35 |
+
- name: Generate API models
|
| 36 |
+
run: |
|
| 37 |
+
datamodel-codegen --use-subclass-enum --input filtered-openapi.yaml --output comfy_api_nodes/apis --output-model-type pydantic_v2.BaseModel
|
| 38 |
+
|
| 39 |
+
- name: Check for changes
|
| 40 |
+
id: git-check
|
| 41 |
+
run: |
|
| 42 |
+
git diff --exit-code comfy_api_nodes/apis || echo "changes=true" >> $GITHUB_OUTPUT
|
| 43 |
+
|
| 44 |
+
- name: Create Pull Request
|
| 45 |
+
if: steps.git-check.outputs.changes == 'true'
|
| 46 |
+
uses: peter-evans/create-pull-request@v5
|
| 47 |
+
with:
|
| 48 |
+
commit-message: 'chore: update API models from OpenAPI spec'
|
| 49 |
+
title: 'Update API models from api.comfy.org'
|
| 50 |
+
body: |
|
| 51 |
+
This PR updates the API models based on the latest api.comfy.org OpenAPI specification.
|
| 52 |
+
|
| 53 |
+
Generated automatically by the a Github workflow.
|
| 54 |
+
branch: update-api-stubs
|
| 55 |
+
delete-branch: true
|
| 56 |
+
base: master
|
.github/workflows/update-ci-container.yml
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "CI: Update CI Container"
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
release:
|
| 5 |
+
types: [published]
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
inputs:
|
| 8 |
+
version:
|
| 9 |
+
description: 'ComfyUI version (e.g., v0.7.0)'
|
| 10 |
+
required: true
|
| 11 |
+
type: string
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
update-ci-container:
|
| 15 |
+
runs-on: ubuntu-latest
|
| 16 |
+
# Skip pre-releases unless manually triggered
|
| 17 |
+
if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
|
| 18 |
+
steps:
|
| 19 |
+
- name: Get version
|
| 20 |
+
id: version
|
| 21 |
+
run: |
|
| 22 |
+
if [ "${{ github.event_name }}" = "release" ]; then
|
| 23 |
+
VERSION="${{ github.event.release.tag_name }}"
|
| 24 |
+
else
|
| 25 |
+
VERSION="${{ inputs.version }}"
|
| 26 |
+
fi
|
| 27 |
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
| 28 |
+
|
| 29 |
+
- name: Checkout comfyui-ci-container
|
| 30 |
+
uses: actions/checkout@v4
|
| 31 |
+
with:
|
| 32 |
+
repository: comfy-org/comfyui-ci-container
|
| 33 |
+
token: ${{ secrets.CI_CONTAINER_PAT }}
|
| 34 |
+
|
| 35 |
+
- name: Check current version
|
| 36 |
+
id: current
|
| 37 |
+
run: |
|
| 38 |
+
CURRENT=$(grep -oP 'ARG COMFYUI_VERSION=\K.*' Dockerfile || echo "unknown")
|
| 39 |
+
echo "current_version=$CURRENT" >> $GITHUB_OUTPUT
|
| 40 |
+
|
| 41 |
+
- name: Update Dockerfile
|
| 42 |
+
run: |
|
| 43 |
+
VERSION="${{ steps.version.outputs.version }}"
|
| 44 |
+
sed -i "s/^ARG COMFYUI_VERSION=.*/ARG COMFYUI_VERSION=${VERSION}/" Dockerfile
|
| 45 |
+
|
| 46 |
+
- name: Create Pull Request
|
| 47 |
+
id: create-pr
|
| 48 |
+
uses: peter-evans/create-pull-request@v7
|
| 49 |
+
with:
|
| 50 |
+
token: ${{ secrets.CI_CONTAINER_PAT }}
|
| 51 |
+
branch: automation/comfyui-${{ steps.version.outputs.version }}
|
| 52 |
+
title: "chore: bump ComfyUI to ${{ steps.version.outputs.version }}"
|
| 53 |
+
body: |
|
| 54 |
+
Updates ComfyUI version from `${{ steps.current.outputs.current_version }}` to `${{ steps.version.outputs.version }}`
|
| 55 |
+
|
| 56 |
+
**Triggered by:** ${{ github.event_name == 'release' && format('[Release {0}]({1})', github.event.release.tag_name, github.event.release.html_url) || 'Manual workflow dispatch' }}
|
| 57 |
+
|
| 58 |
+
labels: automation
|
| 59 |
+
commit-message: "chore: bump ComfyUI to ${{ steps.version.outputs.version }}"
|
.github/workflows/update-version.yml
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Update Version File
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
paths:
|
| 6 |
+
- "pyproject.toml"
|
| 7 |
+
branches:
|
| 8 |
+
- master
|
| 9 |
+
- release/**
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
update-version:
|
| 13 |
+
runs-on: ubuntu-latest
|
| 14 |
+
# Don't run on fork PRs
|
| 15 |
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
| 16 |
+
permissions:
|
| 17 |
+
pull-requests: write
|
| 18 |
+
contents: write
|
| 19 |
+
|
| 20 |
+
steps:
|
| 21 |
+
- name: Checkout repository
|
| 22 |
+
uses: actions/checkout@v4
|
| 23 |
+
|
| 24 |
+
- name: Set up Python
|
| 25 |
+
uses: actions/setup-python@v4
|
| 26 |
+
with:
|
| 27 |
+
python-version: "3.11"
|
| 28 |
+
|
| 29 |
+
- name: Install dependencies
|
| 30 |
+
run: |
|
| 31 |
+
python -m pip install --upgrade pip
|
| 32 |
+
|
| 33 |
+
- name: Update comfyui_version.py
|
| 34 |
+
run: |
|
| 35 |
+
# Read version from pyproject.toml and update comfyui_version.py
|
| 36 |
+
python -c '
|
| 37 |
+
import tomllib
|
| 38 |
+
|
| 39 |
+
# Read version from pyproject.toml
|
| 40 |
+
with open("pyproject.toml", "rb") as f:
|
| 41 |
+
config = tomllib.load(f)
|
| 42 |
+
version = config["project"]["version"]
|
| 43 |
+
|
| 44 |
+
# Write version to comfyui_version.py
|
| 45 |
+
with open("comfyui_version.py", "w") as f:
|
| 46 |
+
f.write("# This file is automatically generated by the build process when version is\n")
|
| 47 |
+
f.write("# updated in pyproject.toml.\n")
|
| 48 |
+
f.write(f"__version__ = \"{version}\"\n")
|
| 49 |
+
'
|
| 50 |
+
|
| 51 |
+
- name: Commit changes
|
| 52 |
+
run: |
|
| 53 |
+
git config --local user.name "github-actions"
|
| 54 |
+
git config --local user.email "github-actions@github.com"
|
| 55 |
+
git fetch origin ${{ github.head_ref }}
|
| 56 |
+
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
|
| 57 |
+
git add comfyui_version.py
|
| 58 |
+
git diff --quiet && git diff --staged --quiet || git commit -m "chore: Update comfyui_version.py to match pyproject.toml"
|
| 59 |
+
git push origin HEAD:${{ github.head_ref }}
|
.github/workflows/windows_release_dependencies.yml
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Windows Release dependencies"
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
inputs:
|
| 6 |
+
xformers:
|
| 7 |
+
description: 'xformers version'
|
| 8 |
+
required: false
|
| 9 |
+
type: string
|
| 10 |
+
default: ""
|
| 11 |
+
extra_dependencies:
|
| 12 |
+
description: 'extra dependencies'
|
| 13 |
+
required: false
|
| 14 |
+
type: string
|
| 15 |
+
default: ""
|
| 16 |
+
cu:
|
| 17 |
+
description: 'cuda version'
|
| 18 |
+
required: true
|
| 19 |
+
type: string
|
| 20 |
+
default: "130"
|
| 21 |
+
|
| 22 |
+
python_minor:
|
| 23 |
+
description: 'python minor version'
|
| 24 |
+
required: true
|
| 25 |
+
type: string
|
| 26 |
+
default: "13"
|
| 27 |
+
|
| 28 |
+
python_patch:
|
| 29 |
+
description: 'python patch version'
|
| 30 |
+
required: true
|
| 31 |
+
type: string
|
| 32 |
+
default: "11"
|
| 33 |
+
# push:
|
| 34 |
+
# branches:
|
| 35 |
+
# - master
|
| 36 |
+
|
| 37 |
+
jobs:
|
| 38 |
+
build_dependencies:
|
| 39 |
+
runs-on: windows-latest
|
| 40 |
+
steps:
|
| 41 |
+
- uses: actions/checkout@v4
|
| 42 |
+
- uses: actions/setup-python@v5
|
| 43 |
+
with:
|
| 44 |
+
python-version: 3.${{ inputs.python_minor }}.${{ inputs.python_patch }}
|
| 45 |
+
|
| 46 |
+
- shell: bash
|
| 47 |
+
run: |
|
| 48 |
+
echo "@echo off
|
| 49 |
+
call update_comfyui.bat nopause
|
| 50 |
+
echo -
|
| 51 |
+
echo This will try to update pytorch and all python dependencies.
|
| 52 |
+
echo -
|
| 53 |
+
echo If you just want to update normally, close this and run update_comfyui.bat instead.
|
| 54 |
+
echo -
|
| 55 |
+
pause
|
| 56 |
+
..\python_embeded\python.exe -s -m pip install --upgrade torch torchvision torchaudio ${{ inputs.xformers }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2
|
| 57 |
+
pause" > update_comfyui_and_python_dependencies.bat
|
| 58 |
+
|
| 59 |
+
grep -v comfyui requirements.txt > requirements_nocomfyui.txt
|
| 60 |
+
python -m pip wheel --no-cache-dir torch torchvision torchaudio ${{ inputs.xformers }} ${{ inputs.extra_dependencies }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r requirements_nocomfyui.txt pygit2 -w ./temp_wheel_dir
|
| 61 |
+
python -m pip install --no-cache-dir ./temp_wheel_dir/*
|
| 62 |
+
echo installed basic
|
| 63 |
+
ls -lah temp_wheel_dir
|
| 64 |
+
mv temp_wheel_dir cu${{ inputs.cu }}_python_deps
|
| 65 |
+
tar cf cu${{ inputs.cu }}_python_deps.tar cu${{ inputs.cu }}_python_deps
|
| 66 |
+
|
| 67 |
+
- uses: actions/cache/save@v4
|
| 68 |
+
with:
|
| 69 |
+
path: |
|
| 70 |
+
cu${{ inputs.cu }}_python_deps.tar
|
| 71 |
+
update_comfyui_and_python_dependencies.bat
|
| 72 |
+
key: ${{ runner.os }}-build-cu${{ inputs.cu }}-${{ inputs.python_minor }}
|
.github/workflows/windows_release_dependencies_manual.yml
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Windows Release dependencies Manual"
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
inputs:
|
| 6 |
+
torch_dependencies:
|
| 7 |
+
description: 'torch dependencies'
|
| 8 |
+
required: false
|
| 9 |
+
type: string
|
| 10 |
+
default: "torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu128"
|
| 11 |
+
cache_tag:
|
| 12 |
+
description: 'Cached dependencies tag'
|
| 13 |
+
required: true
|
| 14 |
+
type: string
|
| 15 |
+
default: "cu128"
|
| 16 |
+
|
| 17 |
+
python_minor:
|
| 18 |
+
description: 'python minor version'
|
| 19 |
+
required: true
|
| 20 |
+
type: string
|
| 21 |
+
default: "12"
|
| 22 |
+
|
| 23 |
+
python_patch:
|
| 24 |
+
description: 'python patch version'
|
| 25 |
+
required: true
|
| 26 |
+
type: string
|
| 27 |
+
default: "10"
|
| 28 |
+
|
| 29 |
+
jobs:
|
| 30 |
+
build_dependencies:
|
| 31 |
+
runs-on: windows-latest
|
| 32 |
+
steps:
|
| 33 |
+
- uses: actions/checkout@v4
|
| 34 |
+
- uses: actions/setup-python@v5
|
| 35 |
+
with:
|
| 36 |
+
python-version: 3.${{ inputs.python_minor }}.${{ inputs.python_patch }}
|
| 37 |
+
|
| 38 |
+
- shell: bash
|
| 39 |
+
run: |
|
| 40 |
+
echo "@echo off
|
| 41 |
+
call update_comfyui.bat nopause
|
| 42 |
+
echo -
|
| 43 |
+
echo This will try to update pytorch and all python dependencies.
|
| 44 |
+
echo -
|
| 45 |
+
echo If you just want to update normally, close this and run update_comfyui.bat instead.
|
| 46 |
+
echo -
|
| 47 |
+
pause
|
| 48 |
+
..\python_embeded\python.exe -s -m pip install --upgrade ${{ inputs.torch_dependencies }} -r ../ComfyUI/requirements.txt pygit2
|
| 49 |
+
pause" > update_comfyui_and_python_dependencies.bat
|
| 50 |
+
|
| 51 |
+
grep -v comfyui requirements.txt > requirements_nocomfyui.txt
|
| 52 |
+
python -m pip wheel --no-cache-dir ${{ inputs.torch_dependencies }} -r requirements_nocomfyui.txt pygit2 -w ./temp_wheel_dir
|
| 53 |
+
python -m pip install --no-cache-dir ./temp_wheel_dir/*
|
| 54 |
+
echo installed basic
|
| 55 |
+
ls -lah temp_wheel_dir
|
| 56 |
+
mv temp_wheel_dir ${{ inputs.cache_tag }}_python_deps
|
| 57 |
+
tar cf ${{ inputs.cache_tag }}_python_deps.tar ${{ inputs.cache_tag }}_python_deps
|
| 58 |
+
|
| 59 |
+
- uses: actions/cache/save@v4
|
| 60 |
+
with:
|
| 61 |
+
path: |
|
| 62 |
+
${{ inputs.cache_tag }}_python_deps.tar
|
| 63 |
+
update_comfyui_and_python_dependencies.bat
|
| 64 |
+
key: ${{ runner.os }}-build-${{ inputs.cache_tag }}-${{ inputs.python_minor }}
|
.github/workflows/windows_release_nightly_pytorch.yml
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Windows Release Nightly pytorch"
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
inputs:
|
| 6 |
+
cu:
|
| 7 |
+
description: 'cuda version'
|
| 8 |
+
required: true
|
| 9 |
+
type: string
|
| 10 |
+
default: "129"
|
| 11 |
+
|
| 12 |
+
python_minor:
|
| 13 |
+
description: 'python minor version'
|
| 14 |
+
required: true
|
| 15 |
+
type: string
|
| 16 |
+
default: "13"
|
| 17 |
+
|
| 18 |
+
python_patch:
|
| 19 |
+
description: 'python patch version'
|
| 20 |
+
required: true
|
| 21 |
+
type: string
|
| 22 |
+
default: "5"
|
| 23 |
+
# push:
|
| 24 |
+
# branches:
|
| 25 |
+
# - master
|
| 26 |
+
|
| 27 |
+
jobs:
|
| 28 |
+
build:
|
| 29 |
+
permissions:
|
| 30 |
+
contents: "write"
|
| 31 |
+
packages: "write"
|
| 32 |
+
pull-requests: "read"
|
| 33 |
+
runs-on: windows-latest
|
| 34 |
+
steps:
|
| 35 |
+
- uses: actions/checkout@v4
|
| 36 |
+
with:
|
| 37 |
+
fetch-depth: 30
|
| 38 |
+
persist-credentials: false
|
| 39 |
+
- uses: actions/setup-python@v5
|
| 40 |
+
with:
|
| 41 |
+
python-version: 3.${{ inputs.python_minor }}.${{ inputs.python_patch }}
|
| 42 |
+
- shell: bash
|
| 43 |
+
run: |
|
| 44 |
+
cd ..
|
| 45 |
+
cp -r ComfyUI ComfyUI_copy
|
| 46 |
+
curl https://www.python.org/ftp/python/3.${{ inputs.python_minor }}.${{ inputs.python_patch }}/python-3.${{ inputs.python_minor }}.${{ inputs.python_patch }}-embed-amd64.zip -o python_embeded.zip
|
| 47 |
+
unzip python_embeded.zip -d python_embeded
|
| 48 |
+
cd python_embeded
|
| 49 |
+
echo 'import site' >> ./python3${{ inputs.python_minor }}._pth
|
| 50 |
+
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
| 51 |
+
./python.exe get-pip.py
|
| 52 |
+
python -m pip wheel torch torchvision torchaudio --pre --extra-index-url https://download.pytorch.org/whl/nightly/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2 -w ../temp_wheel_dir
|
| 53 |
+
ls ../temp_wheel_dir
|
| 54 |
+
./python.exe -s -m pip install --pre ../temp_wheel_dir/*
|
| 55 |
+
sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth
|
| 56 |
+
|
| 57 |
+
rm ./Lib/site-packages/torch/lib/dnnl.lib #I don't think this is actually used and I need the space
|
| 58 |
+
cd ..
|
| 59 |
+
|
| 60 |
+
git clone --depth 1 https://github.com/comfyanonymous/taesd
|
| 61 |
+
cp taesd/*.safetensors ./ComfyUI_copy/models/vae_approx/
|
| 62 |
+
|
| 63 |
+
mkdir ComfyUI_windows_portable_nightly_pytorch
|
| 64 |
+
mv python_embeded ComfyUI_windows_portable_nightly_pytorch
|
| 65 |
+
mv ComfyUI_copy ComfyUI_windows_portable_nightly_pytorch/ComfyUI
|
| 66 |
+
|
| 67 |
+
cd ComfyUI_windows_portable_nightly_pytorch
|
| 68 |
+
|
| 69 |
+
mkdir update
|
| 70 |
+
cp -r ComfyUI/.ci/update_windows/* ./update/
|
| 71 |
+
cp -r ComfyUI/.ci/windows_nvidia_base_files/* ./
|
| 72 |
+
cp -r ComfyUI/.ci/windows_nightly_base_files/* ./
|
| 73 |
+
|
| 74 |
+
echo "call update_comfyui.bat nopause
|
| 75 |
+
..\python_embeded\python.exe -s -m pip install --upgrade --pre torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/nightly/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2
|
| 76 |
+
pause" > ./update/update_comfyui_and_python_dependencies.bat
|
| 77 |
+
cd ..
|
| 78 |
+
|
| 79 |
+
"C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=512m -ms=on -mf=BCJ2 ComfyUI_windows_portable_nightly_pytorch.7z ComfyUI_windows_portable_nightly_pytorch
|
| 80 |
+
mv ComfyUI_windows_portable_nightly_pytorch.7z ComfyUI/ComfyUI_windows_portable_nvidia_or_cpu_nightly_pytorch.7z
|
| 81 |
+
|
| 82 |
+
cd ComfyUI_windows_portable_nightly_pytorch
|
| 83 |
+
python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu
|
| 84 |
+
|
| 85 |
+
ls
|
| 86 |
+
|
| 87 |
+
- name: Upload binaries to release
|
| 88 |
+
uses: svenstaro/upload-release-action@v2
|
| 89 |
+
with:
|
| 90 |
+
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
| 91 |
+
file: ComfyUI_windows_portable_nvidia_or_cpu_nightly_pytorch.7z
|
| 92 |
+
tag: "latest"
|
| 93 |
+
overwrite: true
|
.github/workflows/windows_release_package.yml
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Windows Release packaging"
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
inputs:
|
| 6 |
+
cu:
|
| 7 |
+
description: 'cuda version'
|
| 8 |
+
required: true
|
| 9 |
+
type: string
|
| 10 |
+
default: "129"
|
| 11 |
+
|
| 12 |
+
python_minor:
|
| 13 |
+
description: 'python minor version'
|
| 14 |
+
required: true
|
| 15 |
+
type: string
|
| 16 |
+
default: "13"
|
| 17 |
+
|
| 18 |
+
python_patch:
|
| 19 |
+
description: 'python patch version'
|
| 20 |
+
required: true
|
| 21 |
+
type: string
|
| 22 |
+
default: "6"
|
| 23 |
+
# push:
|
| 24 |
+
# branches:
|
| 25 |
+
# - master
|
| 26 |
+
|
| 27 |
+
jobs:
|
| 28 |
+
package_comfyui:
|
| 29 |
+
permissions:
|
| 30 |
+
contents: "write"
|
| 31 |
+
packages: "write"
|
| 32 |
+
pull-requests: "read"
|
| 33 |
+
runs-on: windows-latest
|
| 34 |
+
steps:
|
| 35 |
+
- uses: actions/cache/restore@v4
|
| 36 |
+
id: cache
|
| 37 |
+
with:
|
| 38 |
+
path: |
|
| 39 |
+
cu${{ inputs.cu }}_python_deps.tar
|
| 40 |
+
update_comfyui_and_python_dependencies.bat
|
| 41 |
+
key: ${{ runner.os }}-build-cu${{ inputs.cu }}-${{ inputs.python_minor }}
|
| 42 |
+
- shell: bash
|
| 43 |
+
run: |
|
| 44 |
+
mv cu${{ inputs.cu }}_python_deps.tar ../
|
| 45 |
+
mv update_comfyui_and_python_dependencies.bat ../
|
| 46 |
+
cd ..
|
| 47 |
+
tar xf cu${{ inputs.cu }}_python_deps.tar
|
| 48 |
+
pwd
|
| 49 |
+
ls
|
| 50 |
+
|
| 51 |
+
- uses: actions/checkout@v4
|
| 52 |
+
with:
|
| 53 |
+
fetch-depth: 150
|
| 54 |
+
persist-credentials: false
|
| 55 |
+
- shell: bash
|
| 56 |
+
run: |
|
| 57 |
+
cd ..
|
| 58 |
+
cp -r ComfyUI ComfyUI_copy
|
| 59 |
+
curl https://www.python.org/ftp/python/3.${{ inputs.python_minor }}.${{ inputs.python_patch }}/python-3.${{ inputs.python_minor }}.${{ inputs.python_patch }}-embed-amd64.zip -o python_embeded.zip
|
| 60 |
+
unzip python_embeded.zip -d python_embeded
|
| 61 |
+
cd python_embeded
|
| 62 |
+
echo 'import site' >> ./python3${{ inputs.python_minor }}._pth
|
| 63 |
+
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
| 64 |
+
./python.exe get-pip.py
|
| 65 |
+
./python.exe -s -m pip install ../cu${{ inputs.cu }}_python_deps/*
|
| 66 |
+
sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth
|
| 67 |
+
|
| 68 |
+
rm ./Lib/site-packages/torch/lib/dnnl.lib #I don't think this is actually used and I need the space
|
| 69 |
+
rm ./Lib/site-packages/torch/lib/libprotoc.lib
|
| 70 |
+
rm ./Lib/site-packages/torch/lib/libprotobuf.lib
|
| 71 |
+
cd ..
|
| 72 |
+
|
| 73 |
+
git clone --depth 1 https://github.com/comfyanonymous/taesd
|
| 74 |
+
cp taesd/*.safetensors ./ComfyUI_copy/models/vae_approx/
|
| 75 |
+
|
| 76 |
+
mkdir ComfyUI_windows_portable
|
| 77 |
+
mv python_embeded ComfyUI_windows_portable
|
| 78 |
+
mv ComfyUI_copy ComfyUI_windows_portable/ComfyUI
|
| 79 |
+
|
| 80 |
+
cd ComfyUI_windows_portable
|
| 81 |
+
|
| 82 |
+
mkdir update
|
| 83 |
+
cp -r ComfyUI/.ci/update_windows/* ./update/
|
| 84 |
+
cp -r ComfyUI/.ci/windows_nvidia_base_files/* ./
|
| 85 |
+
cp ../update_comfyui_and_python_dependencies.bat ./update/
|
| 86 |
+
|
| 87 |
+
cd ..
|
| 88 |
+
|
| 89 |
+
"C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=768m -ms=on -mf=BCJ2 ComfyUI_windows_portable.7z ComfyUI_windows_portable
|
| 90 |
+
mv ComfyUI_windows_portable.7z ComfyUI/new_ComfyUI_windows_portable_nvidia_cu${{ inputs.cu }}_or_cpu.7z
|
| 91 |
+
|
| 92 |
+
cd ComfyUI_windows_portable
|
| 93 |
+
python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu
|
| 94 |
+
|
| 95 |
+
python_embeded/python.exe -s ./update/update.py ComfyUI/
|
| 96 |
+
|
| 97 |
+
ls
|
| 98 |
+
|
| 99 |
+
- name: Upload binaries to release
|
| 100 |
+
uses: svenstaro/upload-release-action@v2
|
| 101 |
+
with:
|
| 102 |
+
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
| 103 |
+
file: new_ComfyUI_windows_portable_nvidia_cu${{ inputs.cu }}_or_cpu.7z
|
| 104 |
+
tag: "latest"
|
| 105 |
+
overwrite: true
|
| 106 |
+
|
comfy/audio_encoders/audio_encoders.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .wav2vec2 import Wav2Vec2Model
|
| 2 |
+
from .whisper import WhisperLargeV3
|
| 3 |
+
import comfy.model_management
|
| 4 |
+
import comfy.ops
|
| 5 |
+
import comfy.utils
|
| 6 |
+
import logging
|
| 7 |
+
import torchaudio
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class AudioEncoderModel():
|
| 11 |
+
def __init__(self, config):
|
| 12 |
+
self.load_device = comfy.model_management.text_encoder_device()
|
| 13 |
+
offload_device = comfy.model_management.text_encoder_offload_device()
|
| 14 |
+
self.dtype = comfy.model_management.text_encoder_dtype(self.load_device)
|
| 15 |
+
model_type = config.pop("model_type")
|
| 16 |
+
model_config = dict(config)
|
| 17 |
+
model_config.update({
|
| 18 |
+
"dtype": self.dtype,
|
| 19 |
+
"device": offload_device,
|
| 20 |
+
"operations": comfy.ops.manual_cast
|
| 21 |
+
})
|
| 22 |
+
|
| 23 |
+
if model_type == "wav2vec2":
|
| 24 |
+
self.model = Wav2Vec2Model(**model_config)
|
| 25 |
+
elif model_type == "whisper3":
|
| 26 |
+
self.model = WhisperLargeV3(**model_config)
|
| 27 |
+
self.model.eval()
|
| 28 |
+
self.patcher = comfy.model_patcher.CoreModelPatcher(self.model, load_device=self.load_device, offload_device=offload_device)
|
| 29 |
+
self.model_sample_rate = 16000
|
| 30 |
+
comfy.model_management.archive_model_dtypes(self.model)
|
| 31 |
+
|
| 32 |
+
def load_sd(self, sd):
|
| 33 |
+
return self.model.load_state_dict(sd, strict=False, assign=self.patcher.is_dynamic())
|
| 34 |
+
|
| 35 |
+
def get_sd(self):
|
| 36 |
+
return self.model.state_dict()
|
| 37 |
+
|
| 38 |
+
def encode_audio(self, audio, sample_rate):
|
| 39 |
+
comfy.model_management.load_model_gpu(self.patcher)
|
| 40 |
+
audio = torchaudio.functional.resample(audio, sample_rate, self.model_sample_rate)
|
| 41 |
+
out, all_layers = self.model(audio.to(self.load_device))
|
| 42 |
+
outputs = {}
|
| 43 |
+
outputs["encoded_audio"] = out
|
| 44 |
+
outputs["encoded_audio_all_layers"] = all_layers
|
| 45 |
+
outputs["audio_samples"] = audio.shape[2]
|
| 46 |
+
return outputs
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def load_audio_encoder_from_sd(sd, prefix=""):
|
| 50 |
+
sd = comfy.utils.state_dict_prefix_replace(sd, {"wav2vec2.": ""})
|
| 51 |
+
if "encoder.layer_norm.bias" in sd: #wav2vec2
|
| 52 |
+
embed_dim = sd["encoder.layer_norm.bias"].shape[0]
|
| 53 |
+
if embed_dim == 1024:# large
|
| 54 |
+
config = {
|
| 55 |
+
"model_type": "wav2vec2",
|
| 56 |
+
"embed_dim": 1024,
|
| 57 |
+
"num_heads": 16,
|
| 58 |
+
"num_layers": 24,
|
| 59 |
+
"conv_norm": True,
|
| 60 |
+
"conv_bias": True,
|
| 61 |
+
"do_normalize": True,
|
| 62 |
+
"do_stable_layer_norm": True
|
| 63 |
+
}
|
| 64 |
+
elif embed_dim == 768: # base
|
| 65 |
+
config = {
|
| 66 |
+
"model_type": "wav2vec2",
|
| 67 |
+
"embed_dim": 768,
|
| 68 |
+
"num_heads": 12,
|
| 69 |
+
"num_layers": 12,
|
| 70 |
+
"conv_norm": False,
|
| 71 |
+
"conv_bias": False,
|
| 72 |
+
"do_normalize": False, # chinese-wav2vec2-base has this False
|
| 73 |
+
"do_stable_layer_norm": False
|
| 74 |
+
}
|
| 75 |
+
else:
|
| 76 |
+
raise RuntimeError("ERROR: audio encoder file is invalid or unsupported embed_dim: {}".format(embed_dim))
|
| 77 |
+
elif "model.encoder.embed_positions.weight" in sd:
|
| 78 |
+
sd = comfy.utils.state_dict_prefix_replace(sd, {"model.": ""})
|
| 79 |
+
config = {
|
| 80 |
+
"model_type": "whisper3",
|
| 81 |
+
}
|
| 82 |
+
else:
|
| 83 |
+
raise RuntimeError("ERROR: audio encoder not supported.")
|
| 84 |
+
|
| 85 |
+
audio_encoder = AudioEncoderModel(config)
|
| 86 |
+
m, u = audio_encoder.load_sd(sd)
|
| 87 |
+
if len(m) > 0:
|
| 88 |
+
logging.warning("missing audio encoder: {}".format(m))
|
| 89 |
+
if len(u) > 0:
|
| 90 |
+
logging.warning("unexpected audio encoder: {}".format(u))
|
| 91 |
+
|
| 92 |
+
return audio_encoder
|
comfy/audio_encoders/wav2vec2.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from comfy.ldm.modules.attention import optimized_attention_masked
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class LayerNormConv(nn.Module):
|
| 7 |
+
def __init__(self, in_channels, out_channels, kernel_size, stride, bias=False, dtype=None, device=None, operations=None):
|
| 8 |
+
super().__init__()
|
| 9 |
+
self.conv = operations.Conv1d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, bias=bias, device=device, dtype=dtype)
|
| 10 |
+
self.layer_norm = operations.LayerNorm(out_channels, elementwise_affine=True, device=device, dtype=dtype)
|
| 11 |
+
|
| 12 |
+
def forward(self, x):
|
| 13 |
+
x = self.conv(x)
|
| 14 |
+
return torch.nn.functional.gelu(self.layer_norm(x.transpose(-2, -1)).transpose(-2, -1))
|
| 15 |
+
|
| 16 |
+
class LayerGroupNormConv(nn.Module):
|
| 17 |
+
def __init__(self, in_channels, out_channels, kernel_size, stride, bias=False, dtype=None, device=None, operations=None):
|
| 18 |
+
super().__init__()
|
| 19 |
+
self.conv = operations.Conv1d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, bias=bias, device=device, dtype=dtype)
|
| 20 |
+
self.layer_norm = operations.GroupNorm(num_groups=out_channels, num_channels=out_channels, affine=True, device=device, dtype=dtype)
|
| 21 |
+
|
| 22 |
+
def forward(self, x):
|
| 23 |
+
x = self.conv(x)
|
| 24 |
+
return torch.nn.functional.gelu(self.layer_norm(x))
|
| 25 |
+
|
| 26 |
+
class ConvNoNorm(nn.Module):
|
| 27 |
+
def __init__(self, in_channels, out_channels, kernel_size, stride, bias=False, dtype=None, device=None, operations=None):
|
| 28 |
+
super().__init__()
|
| 29 |
+
self.conv = operations.Conv1d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, bias=bias, device=device, dtype=dtype)
|
| 30 |
+
|
| 31 |
+
def forward(self, x):
|
| 32 |
+
x = self.conv(x)
|
| 33 |
+
return torch.nn.functional.gelu(x)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class ConvFeatureEncoder(nn.Module):
|
| 37 |
+
def __init__(self, conv_dim, conv_bias=False, conv_norm=True, dtype=None, device=None, operations=None):
|
| 38 |
+
super().__init__()
|
| 39 |
+
if conv_norm:
|
| 40 |
+
self.conv_layers = nn.ModuleList([
|
| 41 |
+
LayerNormConv(1, conv_dim, kernel_size=10, stride=5, bias=True, device=device, dtype=dtype, operations=operations),
|
| 42 |
+
LayerNormConv(conv_dim, conv_dim, kernel_size=3, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 43 |
+
LayerNormConv(conv_dim, conv_dim, kernel_size=3, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 44 |
+
LayerNormConv(conv_dim, conv_dim, kernel_size=3, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 45 |
+
LayerNormConv(conv_dim, conv_dim, kernel_size=3, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 46 |
+
LayerNormConv(conv_dim, conv_dim, kernel_size=2, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 47 |
+
LayerNormConv(conv_dim, conv_dim, kernel_size=2, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 48 |
+
])
|
| 49 |
+
else:
|
| 50 |
+
self.conv_layers = nn.ModuleList([
|
| 51 |
+
LayerGroupNormConv(1, conv_dim, kernel_size=10, stride=5, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 52 |
+
ConvNoNorm(conv_dim, conv_dim, kernel_size=3, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 53 |
+
ConvNoNorm(conv_dim, conv_dim, kernel_size=3, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 54 |
+
ConvNoNorm(conv_dim, conv_dim, kernel_size=3, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 55 |
+
ConvNoNorm(conv_dim, conv_dim, kernel_size=3, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 56 |
+
ConvNoNorm(conv_dim, conv_dim, kernel_size=2, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 57 |
+
ConvNoNorm(conv_dim, conv_dim, kernel_size=2, stride=2, bias=conv_bias, device=device, dtype=dtype, operations=operations),
|
| 58 |
+
])
|
| 59 |
+
|
| 60 |
+
def forward(self, x):
|
| 61 |
+
x = x.unsqueeze(1)
|
| 62 |
+
|
| 63 |
+
for conv in self.conv_layers:
|
| 64 |
+
x = conv(x)
|
| 65 |
+
|
| 66 |
+
return x.transpose(1, 2)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class FeatureProjection(nn.Module):
|
| 70 |
+
def __init__(self, conv_dim, embed_dim, dtype=None, device=None, operations=None):
|
| 71 |
+
super().__init__()
|
| 72 |
+
self.layer_norm = operations.LayerNorm(conv_dim, eps=1e-05, device=device, dtype=dtype)
|
| 73 |
+
self.projection = operations.Linear(conv_dim, embed_dim, device=device, dtype=dtype)
|
| 74 |
+
|
| 75 |
+
def forward(self, x):
|
| 76 |
+
x = self.layer_norm(x)
|
| 77 |
+
x = self.projection(x)
|
| 78 |
+
return x
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class PositionalConvEmbedding(nn.Module):
|
| 82 |
+
def __init__(self, embed_dim=768, kernel_size=128, groups=16):
|
| 83 |
+
super().__init__()
|
| 84 |
+
self.conv = nn.Conv1d(
|
| 85 |
+
embed_dim,
|
| 86 |
+
embed_dim,
|
| 87 |
+
kernel_size=kernel_size,
|
| 88 |
+
padding=kernel_size // 2,
|
| 89 |
+
groups=groups,
|
| 90 |
+
)
|
| 91 |
+
self.conv = torch.nn.utils.parametrizations.weight_norm(self.conv, name="weight", dim=2)
|
| 92 |
+
self.activation = nn.GELU()
|
| 93 |
+
|
| 94 |
+
def forward(self, x):
|
| 95 |
+
x = x.transpose(1, 2)
|
| 96 |
+
x = self.conv(x)[:, :, :-1]
|
| 97 |
+
x = self.activation(x)
|
| 98 |
+
x = x.transpose(1, 2)
|
| 99 |
+
return x
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class TransformerEncoder(nn.Module):
|
| 103 |
+
def __init__(
|
| 104 |
+
self,
|
| 105 |
+
embed_dim=768,
|
| 106 |
+
num_heads=12,
|
| 107 |
+
num_layers=12,
|
| 108 |
+
mlp_ratio=4.0,
|
| 109 |
+
do_stable_layer_norm=True,
|
| 110 |
+
dtype=None, device=None, operations=None
|
| 111 |
+
):
|
| 112 |
+
super().__init__()
|
| 113 |
+
|
| 114 |
+
self.pos_conv_embed = PositionalConvEmbedding(embed_dim=embed_dim)
|
| 115 |
+
self.layers = nn.ModuleList([
|
| 116 |
+
TransformerEncoderLayer(
|
| 117 |
+
embed_dim=embed_dim,
|
| 118 |
+
num_heads=num_heads,
|
| 119 |
+
mlp_ratio=mlp_ratio,
|
| 120 |
+
do_stable_layer_norm=do_stable_layer_norm,
|
| 121 |
+
device=device, dtype=dtype, operations=operations
|
| 122 |
+
)
|
| 123 |
+
for _ in range(num_layers)
|
| 124 |
+
])
|
| 125 |
+
|
| 126 |
+
self.layer_norm = operations.LayerNorm(embed_dim, eps=1e-05, device=device, dtype=dtype)
|
| 127 |
+
self.do_stable_layer_norm = do_stable_layer_norm
|
| 128 |
+
|
| 129 |
+
def forward(self, x, mask=None):
|
| 130 |
+
x = x + self.pos_conv_embed(x)
|
| 131 |
+
all_x = ()
|
| 132 |
+
if not self.do_stable_layer_norm:
|
| 133 |
+
x = self.layer_norm(x)
|
| 134 |
+
for layer in self.layers:
|
| 135 |
+
all_x += (x,)
|
| 136 |
+
x = layer(x, mask)
|
| 137 |
+
if self.do_stable_layer_norm:
|
| 138 |
+
x = self.layer_norm(x)
|
| 139 |
+
all_x += (x,)
|
| 140 |
+
return x, all_x
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class Attention(nn.Module):
|
| 144 |
+
def __init__(self, embed_dim, num_heads, bias=True, dtype=None, device=None, operations=None):
|
| 145 |
+
super().__init__()
|
| 146 |
+
self.embed_dim = embed_dim
|
| 147 |
+
self.num_heads = num_heads
|
| 148 |
+
self.head_dim = embed_dim // num_heads
|
| 149 |
+
|
| 150 |
+
self.k_proj = operations.Linear(embed_dim, embed_dim, bias=bias, device=device, dtype=dtype)
|
| 151 |
+
self.v_proj = operations.Linear(embed_dim, embed_dim, bias=bias, device=device, dtype=dtype)
|
| 152 |
+
self.q_proj = operations.Linear(embed_dim, embed_dim, bias=bias, device=device, dtype=dtype)
|
| 153 |
+
self.out_proj = operations.Linear(embed_dim, embed_dim, bias=bias, device=device, dtype=dtype)
|
| 154 |
+
|
| 155 |
+
def forward(self, x, mask=None):
|
| 156 |
+
assert (mask is None) # TODO?
|
| 157 |
+
q = self.q_proj(x)
|
| 158 |
+
k = self.k_proj(x)
|
| 159 |
+
v = self.v_proj(x)
|
| 160 |
+
|
| 161 |
+
out = optimized_attention_masked(q, k, v, self.num_heads)
|
| 162 |
+
return self.out_proj(out)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
class FeedForward(nn.Module):
|
| 166 |
+
def __init__(self, embed_dim, mlp_ratio, dtype=None, device=None, operations=None):
|
| 167 |
+
super().__init__()
|
| 168 |
+
self.intermediate_dense = operations.Linear(embed_dim, int(embed_dim * mlp_ratio), device=device, dtype=dtype)
|
| 169 |
+
self.output_dense = operations.Linear(int(embed_dim * mlp_ratio), embed_dim, device=device, dtype=dtype)
|
| 170 |
+
|
| 171 |
+
def forward(self, x):
|
| 172 |
+
x = self.intermediate_dense(x)
|
| 173 |
+
x = torch.nn.functional.gelu(x)
|
| 174 |
+
x = self.output_dense(x)
|
| 175 |
+
return x
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
class TransformerEncoderLayer(nn.Module):
|
| 179 |
+
def __init__(
|
| 180 |
+
self,
|
| 181 |
+
embed_dim=768,
|
| 182 |
+
num_heads=12,
|
| 183 |
+
mlp_ratio=4.0,
|
| 184 |
+
do_stable_layer_norm=True,
|
| 185 |
+
dtype=None, device=None, operations=None
|
| 186 |
+
):
|
| 187 |
+
super().__init__()
|
| 188 |
+
|
| 189 |
+
self.attention = Attention(embed_dim, num_heads, device=device, dtype=dtype, operations=operations)
|
| 190 |
+
|
| 191 |
+
self.layer_norm = operations.LayerNorm(embed_dim, device=device, dtype=dtype)
|
| 192 |
+
self.feed_forward = FeedForward(embed_dim, mlp_ratio, device=device, dtype=dtype, operations=operations)
|
| 193 |
+
self.final_layer_norm = operations.LayerNorm(embed_dim, device=device, dtype=dtype)
|
| 194 |
+
self.do_stable_layer_norm = do_stable_layer_norm
|
| 195 |
+
|
| 196 |
+
def forward(self, x, mask=None):
|
| 197 |
+
residual = x
|
| 198 |
+
if self.do_stable_layer_norm:
|
| 199 |
+
x = self.layer_norm(x)
|
| 200 |
+
x = self.attention(x, mask=mask)
|
| 201 |
+
x = residual + x
|
| 202 |
+
if not self.do_stable_layer_norm:
|
| 203 |
+
x = self.layer_norm(x)
|
| 204 |
+
return self.final_layer_norm(x + self.feed_forward(x))
|
| 205 |
+
else:
|
| 206 |
+
return x + self.feed_forward(self.final_layer_norm(x))
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class Wav2Vec2Model(nn.Module):
|
| 210 |
+
"""Complete Wav2Vec 2.0 model."""
|
| 211 |
+
|
| 212 |
+
def __init__(
|
| 213 |
+
self,
|
| 214 |
+
embed_dim=1024,
|
| 215 |
+
final_dim=256,
|
| 216 |
+
num_heads=16,
|
| 217 |
+
num_layers=24,
|
| 218 |
+
conv_norm=True,
|
| 219 |
+
conv_bias=True,
|
| 220 |
+
do_normalize=True,
|
| 221 |
+
do_stable_layer_norm=True,
|
| 222 |
+
dtype=None, device=None, operations=None
|
| 223 |
+
):
|
| 224 |
+
super().__init__()
|
| 225 |
+
|
| 226 |
+
conv_dim = 512
|
| 227 |
+
self.feature_extractor = ConvFeatureEncoder(conv_dim, conv_norm=conv_norm, conv_bias=conv_bias, device=device, dtype=dtype, operations=operations)
|
| 228 |
+
self.feature_projection = FeatureProjection(conv_dim, embed_dim, device=device, dtype=dtype, operations=operations)
|
| 229 |
+
|
| 230 |
+
self.masked_spec_embed = nn.Parameter(torch.empty(embed_dim, device=device, dtype=dtype))
|
| 231 |
+
self.do_normalize = do_normalize
|
| 232 |
+
|
| 233 |
+
self.encoder = TransformerEncoder(
|
| 234 |
+
embed_dim=embed_dim,
|
| 235 |
+
num_heads=num_heads,
|
| 236 |
+
num_layers=num_layers,
|
| 237 |
+
do_stable_layer_norm=do_stable_layer_norm,
|
| 238 |
+
device=device, dtype=dtype, operations=operations
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
def forward(self, x, mask_time_indices=None, return_dict=False):
|
| 242 |
+
x = torch.mean(x, dim=1)
|
| 243 |
+
|
| 244 |
+
if self.do_normalize:
|
| 245 |
+
x = (x - x.mean()) / torch.sqrt(x.var() + 1e-7)
|
| 246 |
+
|
| 247 |
+
features = self.feature_extractor(x)
|
| 248 |
+
features = self.feature_projection(features)
|
| 249 |
+
batch_size, seq_len, _ = features.shape
|
| 250 |
+
|
| 251 |
+
x, all_x = self.encoder(features)
|
| 252 |
+
return x, all_x
|
comfy/audio_encoders/whisper.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
import torchaudio
|
| 5 |
+
from typing import Optional
|
| 6 |
+
from comfy.ldm.modules.attention import optimized_attention_masked
|
| 7 |
+
import comfy.ops
|
| 8 |
+
|
| 9 |
+
class WhisperFeatureExtractor(nn.Module):
|
| 10 |
+
def __init__(self, n_mels=128, device=None):
|
| 11 |
+
super().__init__()
|
| 12 |
+
self.sample_rate = 16000
|
| 13 |
+
self.n_fft = 400
|
| 14 |
+
self.hop_length = 160
|
| 15 |
+
self.n_mels = n_mels
|
| 16 |
+
self.chunk_length = 30
|
| 17 |
+
self.n_samples = 480000
|
| 18 |
+
|
| 19 |
+
self.mel_spectrogram = torchaudio.transforms.MelSpectrogram(
|
| 20 |
+
sample_rate=self.sample_rate,
|
| 21 |
+
n_fft=self.n_fft,
|
| 22 |
+
hop_length=self.hop_length,
|
| 23 |
+
n_mels=self.n_mels,
|
| 24 |
+
f_min=0,
|
| 25 |
+
f_max=8000,
|
| 26 |
+
norm="slaney",
|
| 27 |
+
mel_scale="slaney",
|
| 28 |
+
).to(device)
|
| 29 |
+
|
| 30 |
+
def __call__(self, audio):
|
| 31 |
+
audio = torch.mean(audio, dim=1)
|
| 32 |
+
batch_size = audio.shape[0]
|
| 33 |
+
processed_audio = []
|
| 34 |
+
|
| 35 |
+
for i in range(batch_size):
|
| 36 |
+
aud = audio[i]
|
| 37 |
+
if aud.shape[0] > self.n_samples:
|
| 38 |
+
aud = aud[:self.n_samples]
|
| 39 |
+
elif aud.shape[0] < self.n_samples:
|
| 40 |
+
aud = F.pad(aud, (0, self.n_samples - aud.shape[0]))
|
| 41 |
+
processed_audio.append(aud)
|
| 42 |
+
|
| 43 |
+
audio = torch.stack(processed_audio)
|
| 44 |
+
|
| 45 |
+
mel_spec = self.mel_spectrogram(audio.to(self.mel_spectrogram.spectrogram.window.device))[:, :, :-1].to(audio.device)
|
| 46 |
+
|
| 47 |
+
log_mel_spec = torch.clamp(mel_spec, min=1e-10).log10()
|
| 48 |
+
log_mel_spec = torch.maximum(log_mel_spec, log_mel_spec.max() - 8.0)
|
| 49 |
+
log_mel_spec = (log_mel_spec + 4.0) / 4.0
|
| 50 |
+
|
| 51 |
+
return log_mel_spec
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class MultiHeadAttention(nn.Module):
|
| 55 |
+
def __init__(self, d_model: int, n_heads: int, dtype=None, device=None, operations=None):
|
| 56 |
+
super().__init__()
|
| 57 |
+
assert d_model % n_heads == 0
|
| 58 |
+
|
| 59 |
+
self.d_model = d_model
|
| 60 |
+
self.n_heads = n_heads
|
| 61 |
+
self.d_k = d_model // n_heads
|
| 62 |
+
|
| 63 |
+
self.q_proj = operations.Linear(d_model, d_model, dtype=dtype, device=device)
|
| 64 |
+
self.k_proj = operations.Linear(d_model, d_model, bias=False, dtype=dtype, device=device)
|
| 65 |
+
self.v_proj = operations.Linear(d_model, d_model, dtype=dtype, device=device)
|
| 66 |
+
self.out_proj = operations.Linear(d_model, d_model, dtype=dtype, device=device)
|
| 67 |
+
|
| 68 |
+
def forward(
|
| 69 |
+
self,
|
| 70 |
+
query: torch.Tensor,
|
| 71 |
+
key: torch.Tensor,
|
| 72 |
+
value: torch.Tensor,
|
| 73 |
+
mask: Optional[torch.Tensor] = None,
|
| 74 |
+
) -> torch.Tensor:
|
| 75 |
+
batch_size, seq_len, _ = query.shape
|
| 76 |
+
|
| 77 |
+
q = self.q_proj(query)
|
| 78 |
+
k = self.k_proj(key)
|
| 79 |
+
v = self.v_proj(value)
|
| 80 |
+
|
| 81 |
+
attn_output = optimized_attention_masked(q, k, v, self.n_heads, mask)
|
| 82 |
+
attn_output = self.out_proj(attn_output)
|
| 83 |
+
|
| 84 |
+
return attn_output
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class EncoderLayer(nn.Module):
|
| 88 |
+
def __init__(self, d_model: int, n_heads: int, d_ff: int, dtype=None, device=None, operations=None):
|
| 89 |
+
super().__init__()
|
| 90 |
+
|
| 91 |
+
self.self_attn = MultiHeadAttention(d_model, n_heads, dtype=dtype, device=device, operations=operations)
|
| 92 |
+
self.self_attn_layer_norm = operations.LayerNorm(d_model, dtype=dtype, device=device)
|
| 93 |
+
|
| 94 |
+
self.fc1 = operations.Linear(d_model, d_ff, dtype=dtype, device=device)
|
| 95 |
+
self.fc2 = operations.Linear(d_ff, d_model, dtype=dtype, device=device)
|
| 96 |
+
self.final_layer_norm = operations.LayerNorm(d_model, dtype=dtype, device=device)
|
| 97 |
+
|
| 98 |
+
def forward(
|
| 99 |
+
self,
|
| 100 |
+
x: torch.Tensor,
|
| 101 |
+
attention_mask: Optional[torch.Tensor] = None
|
| 102 |
+
) -> torch.Tensor:
|
| 103 |
+
residual = x
|
| 104 |
+
x = self.self_attn_layer_norm(x)
|
| 105 |
+
x = self.self_attn(x, x, x, attention_mask)
|
| 106 |
+
x = residual + x
|
| 107 |
+
|
| 108 |
+
residual = x
|
| 109 |
+
x = self.final_layer_norm(x)
|
| 110 |
+
x = self.fc1(x)
|
| 111 |
+
x = F.gelu(x)
|
| 112 |
+
x = self.fc2(x)
|
| 113 |
+
x = residual + x
|
| 114 |
+
|
| 115 |
+
return x
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class AudioEncoder(nn.Module):
|
| 119 |
+
def __init__(
|
| 120 |
+
self,
|
| 121 |
+
n_mels: int = 128,
|
| 122 |
+
n_ctx: int = 1500,
|
| 123 |
+
n_state: int = 1280,
|
| 124 |
+
n_head: int = 20,
|
| 125 |
+
n_layer: int = 32,
|
| 126 |
+
dtype=None,
|
| 127 |
+
device=None,
|
| 128 |
+
operations=None
|
| 129 |
+
):
|
| 130 |
+
super().__init__()
|
| 131 |
+
|
| 132 |
+
self.conv1 = operations.Conv1d(n_mels, n_state, kernel_size=3, padding=1, dtype=dtype, device=device)
|
| 133 |
+
self.conv2 = operations.Conv1d(n_state, n_state, kernel_size=3, stride=2, padding=1, dtype=dtype, device=device)
|
| 134 |
+
|
| 135 |
+
self.embed_positions = operations.Embedding(n_ctx, n_state, dtype=dtype, device=device)
|
| 136 |
+
|
| 137 |
+
self.layers = nn.ModuleList([
|
| 138 |
+
EncoderLayer(n_state, n_head, n_state * 4, dtype=dtype, device=device, operations=operations)
|
| 139 |
+
for _ in range(n_layer)
|
| 140 |
+
])
|
| 141 |
+
|
| 142 |
+
self.layer_norm = operations.LayerNorm(n_state, dtype=dtype, device=device)
|
| 143 |
+
|
| 144 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 145 |
+
x = F.gelu(self.conv1(x))
|
| 146 |
+
x = F.gelu(self.conv2(x))
|
| 147 |
+
|
| 148 |
+
x = x.transpose(1, 2)
|
| 149 |
+
|
| 150 |
+
x = x + comfy.ops.cast_to_input(self.embed_positions.weight[:, :x.shape[1]], x)
|
| 151 |
+
|
| 152 |
+
all_x = ()
|
| 153 |
+
for layer in self.layers:
|
| 154 |
+
all_x += (x,)
|
| 155 |
+
x = layer(x)
|
| 156 |
+
|
| 157 |
+
x = self.layer_norm(x)
|
| 158 |
+
all_x += (x,)
|
| 159 |
+
return x, all_x
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class WhisperLargeV3(nn.Module):
|
| 163 |
+
def __init__(
|
| 164 |
+
self,
|
| 165 |
+
n_mels: int = 128,
|
| 166 |
+
n_audio_ctx: int = 1500,
|
| 167 |
+
n_audio_state: int = 1280,
|
| 168 |
+
n_audio_head: int = 20,
|
| 169 |
+
n_audio_layer: int = 32,
|
| 170 |
+
dtype=None,
|
| 171 |
+
device=None,
|
| 172 |
+
operations=None
|
| 173 |
+
):
|
| 174 |
+
super().__init__()
|
| 175 |
+
|
| 176 |
+
self.feature_extractor = WhisperFeatureExtractor(n_mels=n_mels, device=device)
|
| 177 |
+
|
| 178 |
+
self.encoder = AudioEncoder(
|
| 179 |
+
n_mels, n_audio_ctx, n_audio_state, n_audio_head, n_audio_layer,
|
| 180 |
+
dtype=dtype, device=device, operations=operations
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
def forward(self, audio):
|
| 184 |
+
mel = self.feature_extractor(audio)
|
| 185 |
+
x, all_x = self.encoder(mel)
|
| 186 |
+
return x, all_x
|
comfy/background_removal/birefnet.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "birefnet",
|
| 3 |
+
"image_std": [1.0, 1.0, 1.0],
|
| 4 |
+
"image_mean": [0.0, 0.0, 0.0],
|
| 5 |
+
"image_size": 1024,
|
| 6 |
+
"resize_to_original": true
|
| 7 |
+
}
|
comfy/background_removal/birefnet.py
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import comfy.ops
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
from functools import partial
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from torchvision.ops import deform_conv2d
|
| 8 |
+
from comfy.ldm.modules.attention import optimized_attention_for_device
|
| 9 |
+
|
| 10 |
+
CXT = [3072, 1536, 768, 384][1:][::-1][-3:]
|
| 11 |
+
|
| 12 |
+
class Attention(nn.Module):
|
| 13 |
+
def __init__(self, dim, num_heads=8, qkv_bias=False, qk_scale=None, device=None, dtype=None, operations=None):
|
| 14 |
+
super().__init__()
|
| 15 |
+
|
| 16 |
+
self.dim = dim
|
| 17 |
+
self.num_heads = num_heads
|
| 18 |
+
head_dim = dim // num_heads
|
| 19 |
+
self.scale = qk_scale or head_dim ** -0.5
|
| 20 |
+
|
| 21 |
+
self.q = operations.Linear(dim, dim, bias=qkv_bias, device=device, dtype=dtype)
|
| 22 |
+
self.kv = operations.Linear(dim, dim * 2, bias=qkv_bias, device=device, dtype=dtype)
|
| 23 |
+
self.proj = operations.Linear(dim, dim, device=device, dtype=dtype)
|
| 24 |
+
|
| 25 |
+
def forward(self, x):
|
| 26 |
+
B, N, C = x.shape
|
| 27 |
+
optimized_attention = optimized_attention_for_device(x.device, mask=False, small_input=True)
|
| 28 |
+
q = self.q(x).reshape(B, N, self.num_heads, C // self.num_heads).permute(0, 2, 1, 3)
|
| 29 |
+
kv = self.kv(x).reshape(B, -1, 2, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
|
| 30 |
+
k, v = kv[0], kv[1]
|
| 31 |
+
|
| 32 |
+
x = optimized_attention(
|
| 33 |
+
q, k, v, heads=self.num_heads, skip_output_reshape=True, skip_reshape=True
|
| 34 |
+
).transpose(1, 2).reshape(B, N, C)
|
| 35 |
+
x = self.proj(x)
|
| 36 |
+
|
| 37 |
+
return x
|
| 38 |
+
|
| 39 |
+
class Mlp(nn.Module):
|
| 40 |
+
def __init__(self, in_features, hidden_features=None, out_features=None, device=None, dtype=None, operations=None):
|
| 41 |
+
super().__init__()
|
| 42 |
+
out_features = out_features or in_features
|
| 43 |
+
hidden_features = hidden_features or in_features
|
| 44 |
+
self.fc1 = operations.Linear(in_features, hidden_features, device=device, dtype=dtype)
|
| 45 |
+
self.act = nn.GELU()
|
| 46 |
+
self.fc2 = operations.Linear(hidden_features, out_features, device=device, dtype=dtype)
|
| 47 |
+
|
| 48 |
+
def forward(self, x):
|
| 49 |
+
x = self.fc1(x)
|
| 50 |
+
x = self.act(x)
|
| 51 |
+
x = self.fc2(x)
|
| 52 |
+
return x
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def window_partition(x, window_size):
|
| 56 |
+
B, H, W, C = x.shape
|
| 57 |
+
x = x.view(B, H // window_size, window_size, W // window_size, window_size, C)
|
| 58 |
+
windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C)
|
| 59 |
+
return windows
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def window_reverse(windows, window_size, H, W):
|
| 63 |
+
B = int(windows.shape[0] / (H * W / window_size / window_size))
|
| 64 |
+
x = windows.view(B, H // window_size, W // window_size, window_size, window_size, -1)
|
| 65 |
+
x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1)
|
| 66 |
+
return x
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class WindowAttention(nn.Module):
|
| 70 |
+
def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, device=None, dtype=None, operations=None):
|
| 71 |
+
|
| 72 |
+
super().__init__()
|
| 73 |
+
self.dim = dim
|
| 74 |
+
self.window_size = window_size # Wh, Ww
|
| 75 |
+
self.num_heads = num_heads
|
| 76 |
+
head_dim = dim // num_heads
|
| 77 |
+
self.scale = qk_scale or head_dim ** -0.5
|
| 78 |
+
|
| 79 |
+
self.relative_position_bias_table = nn.Parameter(
|
| 80 |
+
torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads, device=device, dtype=dtype))
|
| 81 |
+
|
| 82 |
+
coords_h = torch.arange(self.window_size[0])
|
| 83 |
+
coords_w = torch.arange(self.window_size[1])
|
| 84 |
+
coords = torch.stack(torch.meshgrid([coords_h, coords_w], indexing='ij')) # 2, Wh, Ww
|
| 85 |
+
coords_flatten = torch.flatten(coords, 1) # 2, Wh*Ww
|
| 86 |
+
relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # 2, Wh*Ww, Wh*Ww
|
| 87 |
+
relative_coords = relative_coords.permute(1, 2, 0).contiguous() # Wh*Ww, Wh*Ww, 2
|
| 88 |
+
relative_coords[:, :, 0] += self.window_size[0] - 1
|
| 89 |
+
relative_coords[:, :, 1] += self.window_size[1] - 1
|
| 90 |
+
relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1
|
| 91 |
+
relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww
|
| 92 |
+
self.register_buffer("relative_position_index", relative_position_index)
|
| 93 |
+
|
| 94 |
+
self.qkv = operations.Linear(dim, dim * 3, bias=qkv_bias, device=device, dtype=dtype)
|
| 95 |
+
self.proj = operations.Linear(dim, dim, device=device, dtype=dtype)
|
| 96 |
+
self.softmax = nn.Softmax(dim=-1)
|
| 97 |
+
|
| 98 |
+
def forward(self, x, mask=None):
|
| 99 |
+
B_, N, C = x.shape
|
| 100 |
+
qkv = self.qkv(x).reshape(B_, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
|
| 101 |
+
q, k, v = qkv[0], qkv[1], qkv[2]
|
| 102 |
+
|
| 103 |
+
q = q * self.scale
|
| 104 |
+
attn = (q @ k.transpose(-2, -1))
|
| 105 |
+
|
| 106 |
+
relative_position_bias = self.relative_position_bias_table[self.relative_position_index.long().view(-1)].view(
|
| 107 |
+
self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1) # Wh*Ww,Wh*Ww,nH
|
| 108 |
+
relative_position_bias = comfy.ops.cast_to_input(relative_position_bias.permute(2, 0, 1).contiguous(), attn) # nH, Wh*Ww, Wh*Ww
|
| 109 |
+
attn = attn + relative_position_bias.unsqueeze(0)
|
| 110 |
+
|
| 111 |
+
if mask is not None:
|
| 112 |
+
nW = mask.shape[0]
|
| 113 |
+
attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(1).unsqueeze(0)
|
| 114 |
+
attn = attn.view(-1, self.num_heads, N, N)
|
| 115 |
+
attn = self.softmax(attn)
|
| 116 |
+
else:
|
| 117 |
+
attn = self.softmax(attn)
|
| 118 |
+
|
| 119 |
+
x = (attn @ v).transpose(1, 2).reshape(B_, N, C)
|
| 120 |
+
x = self.proj(x)
|
| 121 |
+
return x
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class SwinTransformerBlock(nn.Module):
|
| 125 |
+
def __init__(self, dim, num_heads, window_size=7, shift_size=0,
|
| 126 |
+
mlp_ratio=4., qkv_bias=True, qk_scale=None,
|
| 127 |
+
norm_layer=nn.LayerNorm, device=None, dtype=None, operations=None):
|
| 128 |
+
super().__init__()
|
| 129 |
+
self.dim = dim
|
| 130 |
+
self.num_heads = num_heads
|
| 131 |
+
self.window_size = window_size
|
| 132 |
+
self.shift_size = shift_size
|
| 133 |
+
self.mlp_ratio = mlp_ratio
|
| 134 |
+
|
| 135 |
+
self.norm1 = norm_layer(dim, device=device, dtype=dtype)
|
| 136 |
+
self.attn = WindowAttention(
|
| 137 |
+
dim, window_size=(self.window_size, self.window_size), num_heads=num_heads,
|
| 138 |
+
qkv_bias=qkv_bias, qk_scale=qk_scale, device=device, dtype=dtype, operations=operations)
|
| 139 |
+
|
| 140 |
+
self.norm2 = norm_layer(dim, device=device, dtype=dtype)
|
| 141 |
+
mlp_hidden_dim = int(dim * mlp_ratio)
|
| 142 |
+
self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, device=device, dtype=dtype, operations=operations)
|
| 143 |
+
|
| 144 |
+
self.H = None
|
| 145 |
+
self.W = None
|
| 146 |
+
|
| 147 |
+
def forward(self, x, mask_matrix):
|
| 148 |
+
B, L, C = x.shape
|
| 149 |
+
H, W = self.H, self.W
|
| 150 |
+
|
| 151 |
+
shortcut = x
|
| 152 |
+
x = self.norm1(x)
|
| 153 |
+
x = x.view(B, H, W, C)
|
| 154 |
+
|
| 155 |
+
pad_l = pad_t = 0
|
| 156 |
+
pad_r = (self.window_size - W % self.window_size) % self.window_size
|
| 157 |
+
pad_b = (self.window_size - H % self.window_size) % self.window_size
|
| 158 |
+
x = F.pad(x, (0, 0, pad_l, pad_r, pad_t, pad_b))
|
| 159 |
+
_, Hp, Wp, _ = x.shape
|
| 160 |
+
|
| 161 |
+
if self.shift_size > 0:
|
| 162 |
+
shifted_x = torch.roll(x, shifts=(-self.shift_size, -self.shift_size), dims=(1, 2))
|
| 163 |
+
attn_mask = mask_matrix
|
| 164 |
+
else:
|
| 165 |
+
shifted_x = x
|
| 166 |
+
attn_mask = None
|
| 167 |
+
|
| 168 |
+
x_windows = window_partition(shifted_x, self.window_size)
|
| 169 |
+
x_windows = x_windows.view(-1, self.window_size * self.window_size, C)
|
| 170 |
+
|
| 171 |
+
attn_windows = self.attn(x_windows, mask=attn_mask)
|
| 172 |
+
|
| 173 |
+
attn_windows = attn_windows.view(-1, self.window_size, self.window_size, C)
|
| 174 |
+
shifted_x = window_reverse(attn_windows, self.window_size, Hp, Wp) # B H' W' C
|
| 175 |
+
|
| 176 |
+
if self.shift_size > 0:
|
| 177 |
+
x = torch.roll(shifted_x, shifts=(self.shift_size, self.shift_size), dims=(1, 2))
|
| 178 |
+
else:
|
| 179 |
+
x = shifted_x
|
| 180 |
+
|
| 181 |
+
if pad_r > 0 or pad_b > 0:
|
| 182 |
+
x = x[:, :H, :W, :].contiguous()
|
| 183 |
+
|
| 184 |
+
x = x.view(B, H * W, C)
|
| 185 |
+
|
| 186 |
+
x = shortcut + x
|
| 187 |
+
x = x + self.mlp(self.norm2(x))
|
| 188 |
+
|
| 189 |
+
return x
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
class PatchMerging(nn.Module):
|
| 193 |
+
def __init__(self, dim, device=None, dtype=None, operations=None):
|
| 194 |
+
super().__init__()
|
| 195 |
+
self.dim = dim
|
| 196 |
+
self.reduction = operations.Linear(4 * dim, 2 * dim, bias=False, device=device, dtype=dtype)
|
| 197 |
+
self.norm = operations.LayerNorm(4 * dim, device=device, dtype=dtype)
|
| 198 |
+
|
| 199 |
+
def forward(self, x, H, W):
|
| 200 |
+
B, L, C = x.shape
|
| 201 |
+
x = x.view(B, H, W, C)
|
| 202 |
+
|
| 203 |
+
# padding
|
| 204 |
+
pad_input = (H % 2 == 1) or (W % 2 == 1)
|
| 205 |
+
if pad_input:
|
| 206 |
+
x = F.pad(x, (0, 0, 0, W % 2, 0, H % 2))
|
| 207 |
+
|
| 208 |
+
x0 = x[:, 0::2, 0::2, :] # B H/2 W/2 C
|
| 209 |
+
x1 = x[:, 1::2, 0::2, :] # B H/2 W/2 C
|
| 210 |
+
x2 = x[:, 0::2, 1::2, :] # B H/2 W/2 C
|
| 211 |
+
x3 = x[:, 1::2, 1::2, :] # B H/2 W/2 C
|
| 212 |
+
x = torch.cat([x0, x1, x2, x3], -1) # B H/2 W/2 4*C
|
| 213 |
+
x = x.view(B, -1, 4 * C) # B H/2*W/2 4*C
|
| 214 |
+
|
| 215 |
+
x = self.norm(x)
|
| 216 |
+
x = self.reduction(x)
|
| 217 |
+
|
| 218 |
+
return x
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
class BasicLayer(nn.Module):
|
| 222 |
+
def __init__(self,
|
| 223 |
+
dim,
|
| 224 |
+
depth,
|
| 225 |
+
num_heads,
|
| 226 |
+
window_size=7,
|
| 227 |
+
mlp_ratio=4.,
|
| 228 |
+
qkv_bias=True,
|
| 229 |
+
qk_scale=None,
|
| 230 |
+
norm_layer=nn.LayerNorm,
|
| 231 |
+
downsample=None,
|
| 232 |
+
device=None, dtype=None, operations=None):
|
| 233 |
+
super().__init__()
|
| 234 |
+
self.window_size = window_size
|
| 235 |
+
self.shift_size = window_size // 2
|
| 236 |
+
self.depth = depth
|
| 237 |
+
|
| 238 |
+
# build blocks
|
| 239 |
+
self.blocks = nn.ModuleList([
|
| 240 |
+
SwinTransformerBlock(
|
| 241 |
+
dim=dim,
|
| 242 |
+
num_heads=num_heads,
|
| 243 |
+
window_size=window_size,
|
| 244 |
+
shift_size=0 if (i % 2 == 0) else window_size // 2,
|
| 245 |
+
mlp_ratio=mlp_ratio,
|
| 246 |
+
qkv_bias=qkv_bias,
|
| 247 |
+
qk_scale=qk_scale,
|
| 248 |
+
norm_layer=norm_layer,
|
| 249 |
+
device=device, dtype=dtype, operations=operations)
|
| 250 |
+
for i in range(depth)])
|
| 251 |
+
|
| 252 |
+
# patch merging layer
|
| 253 |
+
if downsample is not None:
|
| 254 |
+
self.downsample = downsample(dim=dim, device=device, dtype=dtype, operations=operations)
|
| 255 |
+
else:
|
| 256 |
+
self.downsample = None
|
| 257 |
+
|
| 258 |
+
def forward(self, x, H, W):
|
| 259 |
+
Hp = int(np.ceil(H / self.window_size)) * self.window_size
|
| 260 |
+
Wp = int(np.ceil(W / self.window_size)) * self.window_size
|
| 261 |
+
img_mask = torch.zeros((1, Hp, Wp, 1), device=x.device) # 1 Hp Wp 1
|
| 262 |
+
h_slices = (slice(0, -self.window_size),
|
| 263 |
+
slice(-self.window_size, -self.shift_size),
|
| 264 |
+
slice(-self.shift_size, None))
|
| 265 |
+
w_slices = (slice(0, -self.window_size),
|
| 266 |
+
slice(-self.window_size, -self.shift_size),
|
| 267 |
+
slice(-self.shift_size, None))
|
| 268 |
+
cnt = 0
|
| 269 |
+
for h in h_slices:
|
| 270 |
+
for w in w_slices:
|
| 271 |
+
img_mask[:, h, w, :] = cnt
|
| 272 |
+
cnt += 1
|
| 273 |
+
|
| 274 |
+
mask_windows = window_partition(img_mask, self.window_size)
|
| 275 |
+
mask_windows = mask_windows.view(-1, self.window_size * self.window_size)
|
| 276 |
+
attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2)
|
| 277 |
+
attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0))
|
| 278 |
+
|
| 279 |
+
for blk in self.blocks:
|
| 280 |
+
blk.H, blk.W = H, W
|
| 281 |
+
x = blk(x, attn_mask)
|
| 282 |
+
if self.downsample is not None:
|
| 283 |
+
x_down = self.downsample(x, H, W)
|
| 284 |
+
Wh, Ww = (H + 1) // 2, (W + 1) // 2
|
| 285 |
+
return x, H, W, x_down, Wh, Ww
|
| 286 |
+
else:
|
| 287 |
+
return x, H, W, x, H, W
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
class PatchEmbed(nn.Module):
|
| 291 |
+
def __init__(self, patch_size=4, in_channels=3, embed_dim=96, norm_layer=None, device=None, dtype=None, operations=None):
|
| 292 |
+
super().__init__()
|
| 293 |
+
patch_size = (patch_size, patch_size)
|
| 294 |
+
self.patch_size = patch_size
|
| 295 |
+
|
| 296 |
+
self.in_channels = in_channels
|
| 297 |
+
self.embed_dim = embed_dim
|
| 298 |
+
|
| 299 |
+
self.proj = operations.Conv2d(in_channels, embed_dim, kernel_size=patch_size, stride=patch_size, device=device, dtype=dtype)
|
| 300 |
+
if norm_layer is not None:
|
| 301 |
+
self.norm = norm_layer(embed_dim, device=device, dtype=dtype)
|
| 302 |
+
else:
|
| 303 |
+
self.norm = None
|
| 304 |
+
|
| 305 |
+
def forward(self, x):
|
| 306 |
+
_, _, H, W = x.size()
|
| 307 |
+
if W % self.patch_size[1] != 0:
|
| 308 |
+
x = F.pad(x, (0, self.patch_size[1] - W % self.patch_size[1]))
|
| 309 |
+
if H % self.patch_size[0] != 0:
|
| 310 |
+
x = F.pad(x, (0, 0, 0, self.patch_size[0] - H % self.patch_size[0]))
|
| 311 |
+
|
| 312 |
+
x = self.proj(x) # B C Wh Ww
|
| 313 |
+
if self.norm is not None:
|
| 314 |
+
Wh, Ww = x.size(2), x.size(3)
|
| 315 |
+
x = x.flatten(2).transpose(1, 2)
|
| 316 |
+
x = self.norm(x)
|
| 317 |
+
x = x.transpose(1, 2).view(-1, self.embed_dim, Wh, Ww)
|
| 318 |
+
|
| 319 |
+
return x
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
class SwinTransformer(nn.Module):
|
| 323 |
+
def __init__(self,
|
| 324 |
+
pretrain_img_size=224,
|
| 325 |
+
patch_size=4,
|
| 326 |
+
in_channels=3,
|
| 327 |
+
embed_dim=96,
|
| 328 |
+
depths=[2, 2, 6, 2],
|
| 329 |
+
num_heads=[3, 6, 12, 24],
|
| 330 |
+
window_size=7,
|
| 331 |
+
mlp_ratio=4.,
|
| 332 |
+
qkv_bias=True,
|
| 333 |
+
qk_scale=None,
|
| 334 |
+
patch_norm=True,
|
| 335 |
+
out_indices=(0, 1, 2, 3),
|
| 336 |
+
frozen_stages=-1,
|
| 337 |
+
device=None, dtype=None, operations=None):
|
| 338 |
+
super().__init__()
|
| 339 |
+
|
| 340 |
+
norm_layer = partial(operations.LayerNorm, device=device, dtype=dtype)
|
| 341 |
+
self.pretrain_img_size = pretrain_img_size
|
| 342 |
+
self.num_layers = len(depths)
|
| 343 |
+
self.embed_dim = embed_dim
|
| 344 |
+
self.patch_norm = patch_norm
|
| 345 |
+
self.out_indices = out_indices
|
| 346 |
+
self.frozen_stages = frozen_stages
|
| 347 |
+
|
| 348 |
+
self.patch_embed = PatchEmbed(
|
| 349 |
+
patch_size=patch_size, in_channels=in_channels, embed_dim=embed_dim,
|
| 350 |
+
device=device, dtype=dtype, operations=operations,
|
| 351 |
+
norm_layer=norm_layer if self.patch_norm else None)
|
| 352 |
+
|
| 353 |
+
self.layers = nn.ModuleList()
|
| 354 |
+
for i_layer in range(self.num_layers):
|
| 355 |
+
layer = BasicLayer(
|
| 356 |
+
dim=int(embed_dim * 2 ** i_layer),
|
| 357 |
+
depth=depths[i_layer],
|
| 358 |
+
num_heads=num_heads[i_layer],
|
| 359 |
+
window_size=window_size,
|
| 360 |
+
mlp_ratio=mlp_ratio,
|
| 361 |
+
qkv_bias=qkv_bias,
|
| 362 |
+
qk_scale=qk_scale,
|
| 363 |
+
norm_layer=norm_layer,
|
| 364 |
+
downsample=PatchMerging if (i_layer < self.num_layers - 1) else None,
|
| 365 |
+
device=device, dtype=dtype, operations=operations)
|
| 366 |
+
self.layers.append(layer)
|
| 367 |
+
|
| 368 |
+
num_features = [int(embed_dim * 2 ** i) for i in range(self.num_layers)]
|
| 369 |
+
self.num_features = num_features
|
| 370 |
+
|
| 371 |
+
for i_layer in out_indices:
|
| 372 |
+
layer = norm_layer(num_features[i_layer])
|
| 373 |
+
layer_name = f'norm{i_layer}'
|
| 374 |
+
self.add_module(layer_name, layer)
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def forward(self, x):
|
| 378 |
+
x = self.patch_embed(x)
|
| 379 |
+
|
| 380 |
+
Wh, Ww = x.size(2), x.size(3)
|
| 381 |
+
|
| 382 |
+
outs = []
|
| 383 |
+
x = x.flatten(2).transpose(1, 2)
|
| 384 |
+
for i in range(self.num_layers):
|
| 385 |
+
layer = self.layers[i]
|
| 386 |
+
x_out, H, W, x, Wh, Ww = layer(x, Wh, Ww)
|
| 387 |
+
|
| 388 |
+
if i in self.out_indices:
|
| 389 |
+
norm_layer = getattr(self, f'norm{i}')
|
| 390 |
+
x_out = norm_layer(x_out)
|
| 391 |
+
|
| 392 |
+
out = x_out.view(-1, H, W, self.num_features[i]).permute(0, 3, 1, 2).contiguous()
|
| 393 |
+
outs.append(out)
|
| 394 |
+
|
| 395 |
+
return tuple(outs)
|
| 396 |
+
|
| 397 |
+
class DeformableConv2d(nn.Module):
|
| 398 |
+
def __init__(self,
|
| 399 |
+
in_channels,
|
| 400 |
+
out_channels,
|
| 401 |
+
kernel_size=3,
|
| 402 |
+
stride=1,
|
| 403 |
+
padding=1,
|
| 404 |
+
bias=False, device=None, dtype=None, operations=None):
|
| 405 |
+
|
| 406 |
+
super(DeformableConv2d, self).__init__()
|
| 407 |
+
|
| 408 |
+
kernel_size = kernel_size if type(kernel_size) is tuple else (kernel_size, kernel_size)
|
| 409 |
+
self.stride = stride if type(stride) is tuple else (stride, stride)
|
| 410 |
+
self.padding = padding
|
| 411 |
+
|
| 412 |
+
self.offset_conv = operations.Conv2d(in_channels,
|
| 413 |
+
2 * kernel_size[0] * kernel_size[1],
|
| 414 |
+
kernel_size=kernel_size,
|
| 415 |
+
stride=stride,
|
| 416 |
+
padding=self.padding,
|
| 417 |
+
bias=True, device=device, dtype=dtype)
|
| 418 |
+
|
| 419 |
+
self.modulator_conv = operations.Conv2d(in_channels,
|
| 420 |
+
1 * kernel_size[0] * kernel_size[1],
|
| 421 |
+
kernel_size=kernel_size,
|
| 422 |
+
stride=stride,
|
| 423 |
+
padding=self.padding,
|
| 424 |
+
bias=True, device=device, dtype=dtype)
|
| 425 |
+
|
| 426 |
+
self.regular_conv = operations.Conv2d(in_channels,
|
| 427 |
+
out_channels=out_channels,
|
| 428 |
+
kernel_size=kernel_size,
|
| 429 |
+
stride=stride,
|
| 430 |
+
padding=self.padding,
|
| 431 |
+
bias=bias, device=device, dtype=dtype)
|
| 432 |
+
|
| 433 |
+
def forward(self, x):
|
| 434 |
+
offset = self.offset_conv(x)
|
| 435 |
+
modulator = 2. * torch.sigmoid(self.modulator_conv(x))
|
| 436 |
+
weight, bias, offload_info = comfy.ops.cast_bias_weight(self.regular_conv, x, offloadable=True)
|
| 437 |
+
|
| 438 |
+
x = deform_conv2d(
|
| 439 |
+
input=x,
|
| 440 |
+
offset=offset,
|
| 441 |
+
weight=weight,
|
| 442 |
+
bias=None,
|
| 443 |
+
padding=self.padding,
|
| 444 |
+
mask=modulator,
|
| 445 |
+
stride=self.stride,
|
| 446 |
+
)
|
| 447 |
+
comfy.ops.uncast_bias_weight(self.regular_conv, weight, bias, offload_info)
|
| 448 |
+
return x
|
| 449 |
+
|
| 450 |
+
class BasicDecBlk(nn.Module):
|
| 451 |
+
def __init__(self, in_channels=64, out_channels=64, inter_channels=64, device=None, dtype=None, operations=None):
|
| 452 |
+
super(BasicDecBlk, self).__init__()
|
| 453 |
+
inter_channels = 64
|
| 454 |
+
self.conv_in = operations.Conv2d(in_channels, inter_channels, 3, 1, padding=1, device=device, dtype=dtype)
|
| 455 |
+
self.relu_in = nn.ReLU(inplace=True)
|
| 456 |
+
self.dec_att = ASPPDeformable(in_channels=inter_channels, device=device, dtype=dtype, operations=operations)
|
| 457 |
+
self.conv_out = operations.Conv2d(inter_channels, out_channels, 3, 1, padding=1, device=device, dtype=dtype)
|
| 458 |
+
self.bn_in = operations.BatchNorm2d(inter_channels, device=device, dtype=dtype)
|
| 459 |
+
self.bn_out = operations.BatchNorm2d(out_channels, device=device, dtype=dtype)
|
| 460 |
+
|
| 461 |
+
def forward(self, x):
|
| 462 |
+
x = self.conv_in(x)
|
| 463 |
+
x = self.bn_in(x)
|
| 464 |
+
x = self.relu_in(x)
|
| 465 |
+
x = self.dec_att(x)
|
| 466 |
+
x = self.conv_out(x)
|
| 467 |
+
x = self.bn_out(x)
|
| 468 |
+
return x
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
class BasicLatBlk(nn.Module):
|
| 472 |
+
def __init__(self, in_channels=64, out_channels=64, device=None, dtype=None, operations=None):
|
| 473 |
+
super(BasicLatBlk, self).__init__()
|
| 474 |
+
self.conv = operations.Conv2d(in_channels, out_channels, 1, 1, 0, device=device, dtype=dtype)
|
| 475 |
+
|
| 476 |
+
def forward(self, x):
|
| 477 |
+
x = self.conv(x)
|
| 478 |
+
return x
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
class _ASPPModuleDeformable(nn.Module):
|
| 482 |
+
def __init__(self, in_channels, planes, kernel_size, padding, device, dtype, operations):
|
| 483 |
+
super(_ASPPModuleDeformable, self).__init__()
|
| 484 |
+
self.atrous_conv = DeformableConv2d(in_channels, planes, kernel_size=kernel_size,
|
| 485 |
+
stride=1, padding=padding, bias=False, device=device, dtype=dtype, operations=operations)
|
| 486 |
+
self.bn = operations.BatchNorm2d(planes, device=device, dtype=dtype)
|
| 487 |
+
self.relu = nn.ReLU(inplace=True)
|
| 488 |
+
|
| 489 |
+
def forward(self, x):
|
| 490 |
+
x = self.atrous_conv(x)
|
| 491 |
+
x = self.bn(x)
|
| 492 |
+
|
| 493 |
+
return self.relu(x)
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
class ASPPDeformable(nn.Module):
|
| 497 |
+
def __init__(self, in_channels, out_channels=None, parallel_block_sizes=[1, 3, 7], device=None, dtype=None, operations=None):
|
| 498 |
+
super(ASPPDeformable, self).__init__()
|
| 499 |
+
self.down_scale = 1
|
| 500 |
+
if out_channels is None:
|
| 501 |
+
out_channels = in_channels
|
| 502 |
+
self.in_channelster = 256 // self.down_scale
|
| 503 |
+
|
| 504 |
+
self.aspp1 = _ASPPModuleDeformable(in_channels, self.in_channelster, 1, padding=0, device=device, dtype=dtype, operations=operations)
|
| 505 |
+
self.aspp_deforms = nn.ModuleList([
|
| 506 |
+
_ASPPModuleDeformable(in_channels, self.in_channelster, conv_size, padding=int(conv_size//2), device=device, dtype=dtype, operations=operations)
|
| 507 |
+
for conv_size in parallel_block_sizes
|
| 508 |
+
])
|
| 509 |
+
|
| 510 |
+
self.global_avg_pool = nn.Sequential(nn.AdaptiveAvgPool2d((1, 1)),
|
| 511 |
+
operations.Conv2d(in_channels, self.in_channelster, 1, stride=1, bias=False, device=device, dtype=dtype),
|
| 512 |
+
operations.BatchNorm2d(self.in_channelster, device=device, dtype=dtype),
|
| 513 |
+
nn.ReLU(inplace=True))
|
| 514 |
+
self.conv1 = operations.Conv2d(self.in_channelster * (2 + len(self.aspp_deforms)), out_channels, 1, bias=False, device=device, dtype=dtype)
|
| 515 |
+
self.bn1 = operations.BatchNorm2d(out_channels, device=device, dtype=dtype)
|
| 516 |
+
self.relu = nn.ReLU(inplace=True)
|
| 517 |
+
|
| 518 |
+
def forward(self, x):
|
| 519 |
+
x1 = self.aspp1(x)
|
| 520 |
+
x_aspp_deforms = [aspp_deform(x) for aspp_deform in self.aspp_deforms]
|
| 521 |
+
x5 = self.global_avg_pool(x)
|
| 522 |
+
x5 = F.interpolate(x5, size=x1.size()[2:], mode='bilinear', align_corners=True)
|
| 523 |
+
x = torch.cat((x1, *x_aspp_deforms, x5), dim=1)
|
| 524 |
+
|
| 525 |
+
x = self.conv1(x)
|
| 526 |
+
x = self.bn1(x)
|
| 527 |
+
x = self.relu(x)
|
| 528 |
+
|
| 529 |
+
return x
|
| 530 |
+
|
| 531 |
+
class BiRefNet(nn.Module):
|
| 532 |
+
def __init__(self, config=None, dtype=None, device=None, operations=None):
|
| 533 |
+
super(BiRefNet, self).__init__()
|
| 534 |
+
self.bb = SwinTransformer(embed_dim=192, depths=[2, 2, 18, 2], num_heads=[6, 12, 24, 48], window_size=12, device=device, dtype=dtype, operations=operations)
|
| 535 |
+
|
| 536 |
+
channels = [1536, 768, 384, 192]
|
| 537 |
+
channels = [c * 2 for c in channels]
|
| 538 |
+
self.cxt = channels[1:][::-1][-3:]
|
| 539 |
+
self.squeeze_module = nn.Sequential(*[
|
| 540 |
+
BasicDecBlk(channels[0]+sum(self.cxt), channels[0], device=device, dtype=dtype, operations=operations)
|
| 541 |
+
for _ in range(1)
|
| 542 |
+
])
|
| 543 |
+
|
| 544 |
+
self.decoder = Decoder(channels, device=device, dtype=dtype, operations=operations)
|
| 545 |
+
|
| 546 |
+
def forward_enc(self, x):
|
| 547 |
+
x1, x2, x3, x4 = self.bb(x)
|
| 548 |
+
B, C, H, W = x.shape
|
| 549 |
+
x1_, x2_, x3_, x4_ = self.bb(F.interpolate(x, size=(H//2, W//2), mode='bilinear', align_corners=True))
|
| 550 |
+
x1 = torch.cat([x1, F.interpolate(x1_, size=x1.shape[2:], mode='bilinear', align_corners=True)], dim=1)
|
| 551 |
+
x2 = torch.cat([x2, F.interpolate(x2_, size=x2.shape[2:], mode='bilinear', align_corners=True)], dim=1)
|
| 552 |
+
x3 = torch.cat([x3, F.interpolate(x3_, size=x3.shape[2:], mode='bilinear', align_corners=True)], dim=1)
|
| 553 |
+
x4 = torch.cat([x4, F.interpolate(x4_, size=x4.shape[2:], mode='bilinear', align_corners=True)], dim=1)
|
| 554 |
+
x4 = torch.cat(
|
| 555 |
+
(
|
| 556 |
+
*[
|
| 557 |
+
F.interpolate(x1, size=x4.shape[2:], mode='bilinear', align_corners=True),
|
| 558 |
+
F.interpolate(x2, size=x4.shape[2:], mode='bilinear', align_corners=True),
|
| 559 |
+
F.interpolate(x3, size=x4.shape[2:], mode='bilinear', align_corners=True),
|
| 560 |
+
][-len(CXT):],
|
| 561 |
+
x4
|
| 562 |
+
),
|
| 563 |
+
dim=1
|
| 564 |
+
)
|
| 565 |
+
return (x1, x2, x3, x4)
|
| 566 |
+
|
| 567 |
+
def forward_ori(self, x):
|
| 568 |
+
(x1, x2, x3, x4) = self.forward_enc(x)
|
| 569 |
+
x4 = self.squeeze_module(x4)
|
| 570 |
+
features = [x, x1, x2, x3, x4]
|
| 571 |
+
scaled_preds = self.decoder(features)
|
| 572 |
+
return scaled_preds
|
| 573 |
+
|
| 574 |
+
def forward(self, pixel_values, intermediate_output=None):
|
| 575 |
+
scaled_preds = self.forward_ori(pixel_values)
|
| 576 |
+
return scaled_preds
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
class Decoder(nn.Module):
|
| 580 |
+
def __init__(self, channels, device, dtype, operations):
|
| 581 |
+
super(Decoder, self).__init__()
|
| 582 |
+
# factory kwargs
|
| 583 |
+
fk = {"device":device, "dtype":dtype, "operations":operations}
|
| 584 |
+
DecoderBlock = partial(BasicDecBlk, **fk)
|
| 585 |
+
LateralBlock = partial(BasicLatBlk, **fk)
|
| 586 |
+
DBlock = partial(SimpleConvs, **fk)
|
| 587 |
+
|
| 588 |
+
self.split = True
|
| 589 |
+
N_dec_ipt = 64
|
| 590 |
+
ic = 64
|
| 591 |
+
ipt_cha_opt = 1
|
| 592 |
+
self.ipt_blk5 = DBlock(2**10*3 if self.split else 3, [N_dec_ipt, channels[0]//8][ipt_cha_opt], inter_channels=ic)
|
| 593 |
+
self.ipt_blk4 = DBlock(2**8*3 if self.split else 3, [N_dec_ipt, channels[0]//8][ipt_cha_opt], inter_channels=ic)
|
| 594 |
+
self.ipt_blk3 = DBlock(2**6*3 if self.split else 3, [N_dec_ipt, channels[1]//8][ipt_cha_opt], inter_channels=ic)
|
| 595 |
+
self.ipt_blk2 = DBlock(2**4*3 if self.split else 3, [N_dec_ipt, channels[2]//8][ipt_cha_opt], inter_channels=ic)
|
| 596 |
+
self.ipt_blk1 = DBlock(2**0*3 if self.split else 3, [N_dec_ipt, channels[3]//8][ipt_cha_opt], inter_channels=ic)
|
| 597 |
+
|
| 598 |
+
self.decoder_block4 = DecoderBlock(channels[0]+([N_dec_ipt, channels[0]//8][ipt_cha_opt]), channels[1])
|
| 599 |
+
self.decoder_block3 = DecoderBlock(channels[1]+([N_dec_ipt, channels[0]//8][ipt_cha_opt]), channels[2])
|
| 600 |
+
self.decoder_block2 = DecoderBlock(channels[2]+([N_dec_ipt, channels[1]//8][ipt_cha_opt]), channels[3])
|
| 601 |
+
self.decoder_block1 = DecoderBlock(channels[3]+([N_dec_ipt, channels[2]//8][ipt_cha_opt]), channels[3]//2)
|
| 602 |
+
|
| 603 |
+
fk = {"device":device, "dtype":dtype}
|
| 604 |
+
|
| 605 |
+
self.conv_out1 = nn.Sequential(operations.Conv2d(channels[3]//2+([N_dec_ipt, channels[3]//8][ipt_cha_opt]), 1, 1, 1, 0, **fk))
|
| 606 |
+
|
| 607 |
+
self.lateral_block4 = LateralBlock(channels[1], channels[1])
|
| 608 |
+
self.lateral_block3 = LateralBlock(channels[2], channels[2])
|
| 609 |
+
self.lateral_block2 = LateralBlock(channels[3], channels[3])
|
| 610 |
+
|
| 611 |
+
self.conv_ms_spvn_4 = operations.Conv2d(channels[1], 1, 1, 1, 0, **fk)
|
| 612 |
+
self.conv_ms_spvn_3 = operations.Conv2d(channels[2], 1, 1, 1, 0, **fk)
|
| 613 |
+
self.conv_ms_spvn_2 = operations.Conv2d(channels[3], 1, 1, 1, 0, **fk)
|
| 614 |
+
|
| 615 |
+
_N = 16
|
| 616 |
+
|
| 617 |
+
self.gdt_convs_4 = nn.Sequential(operations.Conv2d(channels[0] // 2, _N, 3, 1, 1, **fk), operations.BatchNorm2d(_N, **fk), nn.ReLU(inplace=True))
|
| 618 |
+
self.gdt_convs_3 = nn.Sequential(operations.Conv2d(channels[1] // 2, _N, 3, 1, 1, **fk), operations.BatchNorm2d(_N, **fk), nn.ReLU(inplace=True))
|
| 619 |
+
self.gdt_convs_2 = nn.Sequential(operations.Conv2d(channels[2] // 2, _N, 3, 1, 1, **fk), operations.BatchNorm2d(_N, **fk), nn.ReLU(inplace=True))
|
| 620 |
+
|
| 621 |
+
[setattr(self, f"gdt_convs_pred_{i}", nn.Sequential(operations.Conv2d(_N, 1, 1, 1, 0, **fk))) for i in range(2, 5)]
|
| 622 |
+
[setattr(self, f"gdt_convs_attn_{i}", nn.Sequential(operations.Conv2d(_N, 1, 1, 1, 0, **fk))) for i in range(2, 5)]
|
| 623 |
+
|
| 624 |
+
def get_patches_batch(self, x, p):
|
| 625 |
+
_size_h, _size_w = p.shape[2:]
|
| 626 |
+
patches_batch = []
|
| 627 |
+
for idx in range(x.shape[0]):
|
| 628 |
+
columns_x = torch.split(x[idx], split_size_or_sections=_size_w, dim=-1)
|
| 629 |
+
patches_x = []
|
| 630 |
+
for column_x in columns_x:
|
| 631 |
+
patches_x += [p.unsqueeze(0) for p in torch.split(column_x, split_size_or_sections=_size_h, dim=-2)]
|
| 632 |
+
patch_sample = torch.cat(patches_x, dim=1)
|
| 633 |
+
patches_batch.append(patch_sample)
|
| 634 |
+
return torch.cat(patches_batch, dim=0)
|
| 635 |
+
|
| 636 |
+
def forward(self, features):
|
| 637 |
+
x, x1, x2, x3, x4 = features
|
| 638 |
+
|
| 639 |
+
patches_batch = self.get_patches_batch(x, x4) if self.split else x
|
| 640 |
+
x4 = torch.cat((x4, self.ipt_blk5(F.interpolate(patches_batch, size=x4.shape[2:], mode='bilinear', align_corners=True))), 1)
|
| 641 |
+
p4 = self.decoder_block4(x4)
|
| 642 |
+
p4_gdt = self.gdt_convs_4(p4)
|
| 643 |
+
gdt_attn_4 = self.gdt_convs_attn_4(p4_gdt).sigmoid()
|
| 644 |
+
p4 = p4 * gdt_attn_4
|
| 645 |
+
_p4 = F.interpolate(p4, size=x3.shape[2:], mode='bilinear', align_corners=True)
|
| 646 |
+
_p3 = _p4 + self.lateral_block4(x3)
|
| 647 |
+
|
| 648 |
+
patches_batch = self.get_patches_batch(x, _p3) if self.split else x
|
| 649 |
+
_p3 = torch.cat((_p3, self.ipt_blk4(F.interpolate(patches_batch, size=x3.shape[2:], mode='bilinear', align_corners=True))), 1)
|
| 650 |
+
p3 = self.decoder_block3(_p3)
|
| 651 |
+
|
| 652 |
+
p3_gdt = self.gdt_convs_3(p3)
|
| 653 |
+
gdt_attn_3 = self.gdt_convs_attn_3(p3_gdt).sigmoid()
|
| 654 |
+
p3 = p3 * gdt_attn_3
|
| 655 |
+
_p3 = F.interpolate(p3, size=x2.shape[2:], mode='bilinear', align_corners=True)
|
| 656 |
+
_p2 = _p3 + self.lateral_block3(x2)
|
| 657 |
+
|
| 658 |
+
patches_batch = self.get_patches_batch(x, _p2) if self.split else x
|
| 659 |
+
_p2 = torch.cat((_p2, self.ipt_blk3(F.interpolate(patches_batch, size=x2.shape[2:], mode='bilinear', align_corners=True))), 1)
|
| 660 |
+
p2 = self.decoder_block2(_p2)
|
| 661 |
+
|
| 662 |
+
p2_gdt = self.gdt_convs_2(p2)
|
| 663 |
+
gdt_attn_2 = self.gdt_convs_attn_2(p2_gdt).sigmoid()
|
| 664 |
+
p2 = p2 * gdt_attn_2
|
| 665 |
+
|
| 666 |
+
_p2 = F.interpolate(p2, size=x1.shape[2:], mode='bilinear', align_corners=True)
|
| 667 |
+
_p1 = _p2 + self.lateral_block2(x1)
|
| 668 |
+
|
| 669 |
+
patches_batch = self.get_patches_batch(x, _p1) if self.split else x
|
| 670 |
+
_p1 = torch.cat((_p1, self.ipt_blk2(F.interpolate(patches_batch, size=x1.shape[2:], mode='bilinear', align_corners=True))), 1)
|
| 671 |
+
_p1 = self.decoder_block1(_p1)
|
| 672 |
+
_p1 = F.interpolate(_p1, size=x.shape[2:], mode='bilinear', align_corners=True)
|
| 673 |
+
|
| 674 |
+
patches_batch = self.get_patches_batch(x, _p1) if self.split else x
|
| 675 |
+
_p1 = torch.cat((_p1, self.ipt_blk1(F.interpolate(patches_batch, size=x.shape[2:], mode='bilinear', align_corners=True))), 1)
|
| 676 |
+
p1_out = self.conv_out1(_p1)
|
| 677 |
+
return p1_out
|
| 678 |
+
|
| 679 |
+
|
| 680 |
+
class SimpleConvs(nn.Module):
|
| 681 |
+
def __init__(
|
| 682 |
+
self, in_channels: int, out_channels: int, inter_channels=64, device=None, dtype=None, operations=None
|
| 683 |
+
) -> None:
|
| 684 |
+
super().__init__()
|
| 685 |
+
self.conv1 = operations.Conv2d(in_channels, inter_channels, 3, 1, 1, device=device, dtype=dtype)
|
| 686 |
+
self.conv_out = operations.Conv2d(inter_channels, out_channels, 3, 1, 1, device=device, dtype=dtype)
|
| 687 |
+
|
| 688 |
+
def forward(self, x):
|
| 689 |
+
return self.conv_out(self.conv1(x))
|
comfy/cldm/cldm.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#taken from: https://github.com/lllyasviel/ControlNet
|
| 2 |
+
#and modified
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
|
| 7 |
+
from ..ldm.modules.diffusionmodules.util import (
|
| 8 |
+
timestep_embedding,
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
from ..ldm.modules.attention import SpatialTransformer
|
| 12 |
+
from ..ldm.modules.diffusionmodules.openaimodel import UNetModel, TimestepEmbedSequential, ResBlock, Downsample
|
| 13 |
+
from ..ldm.util import exists
|
| 14 |
+
from .control_types import UNION_CONTROLNET_TYPES
|
| 15 |
+
from collections import OrderedDict
|
| 16 |
+
import comfy.ops
|
| 17 |
+
from comfy.ldm.modules.attention import optimized_attention
|
| 18 |
+
|
| 19 |
+
class OptimizedAttention(nn.Module):
|
| 20 |
+
def __init__(self, c, nhead, dropout=0.0, dtype=None, device=None, operations=None):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.heads = nhead
|
| 23 |
+
self.c = c
|
| 24 |
+
|
| 25 |
+
self.in_proj = operations.Linear(c, c * 3, bias=True, dtype=dtype, device=device)
|
| 26 |
+
self.out_proj = operations.Linear(c, c, bias=True, dtype=dtype, device=device)
|
| 27 |
+
|
| 28 |
+
def forward(self, x):
|
| 29 |
+
x = self.in_proj(x)
|
| 30 |
+
q, k, v = x.split(self.c, dim=2)
|
| 31 |
+
out = optimized_attention(q, k, v, self.heads)
|
| 32 |
+
return self.out_proj(out)
|
| 33 |
+
|
| 34 |
+
class QuickGELU(nn.Module):
|
| 35 |
+
def forward(self, x: torch.Tensor):
|
| 36 |
+
return x * torch.sigmoid(1.702 * x)
|
| 37 |
+
|
| 38 |
+
class ResBlockUnionControlnet(nn.Module):
|
| 39 |
+
def __init__(self, dim, nhead, dtype=None, device=None, operations=None):
|
| 40 |
+
super().__init__()
|
| 41 |
+
self.attn = OptimizedAttention(dim, nhead, dtype=dtype, device=device, operations=operations)
|
| 42 |
+
self.ln_1 = operations.LayerNorm(dim, dtype=dtype, device=device)
|
| 43 |
+
self.mlp = nn.Sequential(
|
| 44 |
+
OrderedDict([("c_fc", operations.Linear(dim, dim * 4, dtype=dtype, device=device)), ("gelu", QuickGELU()),
|
| 45 |
+
("c_proj", operations.Linear(dim * 4, dim, dtype=dtype, device=device))]))
|
| 46 |
+
self.ln_2 = operations.LayerNorm(dim, dtype=dtype, device=device)
|
| 47 |
+
|
| 48 |
+
def attention(self, x: torch.Tensor):
|
| 49 |
+
return self.attn(x)
|
| 50 |
+
|
| 51 |
+
def forward(self, x: torch.Tensor):
|
| 52 |
+
x = x + self.attention(self.ln_1(x))
|
| 53 |
+
x = x + self.mlp(self.ln_2(x))
|
| 54 |
+
return x
|
| 55 |
+
|
| 56 |
+
class ControlledUnetModel(UNetModel):
|
| 57 |
+
#implemented in the ldm unet
|
| 58 |
+
pass
|
| 59 |
+
|
| 60 |
+
class ControlNet(nn.Module):
|
| 61 |
+
def __init__(
|
| 62 |
+
self,
|
| 63 |
+
image_size,
|
| 64 |
+
in_channels,
|
| 65 |
+
model_channels,
|
| 66 |
+
hint_channels,
|
| 67 |
+
num_res_blocks,
|
| 68 |
+
dropout=0,
|
| 69 |
+
channel_mult=(1, 2, 4, 8),
|
| 70 |
+
conv_resample=True,
|
| 71 |
+
dims=2,
|
| 72 |
+
num_classes=None,
|
| 73 |
+
use_checkpoint=False,
|
| 74 |
+
dtype=torch.float32,
|
| 75 |
+
num_heads=-1,
|
| 76 |
+
num_head_channels=-1,
|
| 77 |
+
num_heads_upsample=-1,
|
| 78 |
+
use_scale_shift_norm=False,
|
| 79 |
+
resblock_updown=False,
|
| 80 |
+
use_new_attention_order=False,
|
| 81 |
+
use_spatial_transformer=False, # custom transformer support
|
| 82 |
+
transformer_depth=1, # custom transformer support
|
| 83 |
+
context_dim=None, # custom transformer support
|
| 84 |
+
n_embed=None, # custom support for prediction of discrete ids into codebook of first stage vq model
|
| 85 |
+
legacy=True,
|
| 86 |
+
disable_self_attentions=None,
|
| 87 |
+
num_attention_blocks=None,
|
| 88 |
+
disable_middle_self_attn=False,
|
| 89 |
+
use_linear_in_transformer=False,
|
| 90 |
+
adm_in_channels=None,
|
| 91 |
+
transformer_depth_middle=None,
|
| 92 |
+
transformer_depth_output=None,
|
| 93 |
+
attn_precision=None,
|
| 94 |
+
union_controlnet_num_control_type=None,
|
| 95 |
+
device=None,
|
| 96 |
+
operations=comfy.ops.disable_weight_init,
|
| 97 |
+
**kwargs,
|
| 98 |
+
):
|
| 99 |
+
super().__init__()
|
| 100 |
+
assert use_spatial_transformer == True, "use_spatial_transformer has to be true"
|
| 101 |
+
if use_spatial_transformer:
|
| 102 |
+
assert context_dim is not None, 'Fool!! You forgot to include the dimension of your cross-attention conditioning...'
|
| 103 |
+
|
| 104 |
+
if context_dim is not None:
|
| 105 |
+
assert use_spatial_transformer, 'Fool!! You forgot to use the spatial transformer for your cross-attention conditioning...'
|
| 106 |
+
# from omegaconf.listconfig import ListConfig
|
| 107 |
+
# if type(context_dim) == ListConfig:
|
| 108 |
+
# context_dim = list(context_dim)
|
| 109 |
+
|
| 110 |
+
if num_heads_upsample == -1:
|
| 111 |
+
num_heads_upsample = num_heads
|
| 112 |
+
|
| 113 |
+
if num_heads == -1:
|
| 114 |
+
assert num_head_channels != -1, 'Either num_heads or num_head_channels has to be set'
|
| 115 |
+
|
| 116 |
+
if num_head_channels == -1:
|
| 117 |
+
assert num_heads != -1, 'Either num_heads or num_head_channels has to be set'
|
| 118 |
+
|
| 119 |
+
self.dims = dims
|
| 120 |
+
self.image_size = image_size
|
| 121 |
+
self.in_channels = in_channels
|
| 122 |
+
self.model_channels = model_channels
|
| 123 |
+
|
| 124 |
+
if isinstance(num_res_blocks, int):
|
| 125 |
+
self.num_res_blocks = len(channel_mult) * [num_res_blocks]
|
| 126 |
+
else:
|
| 127 |
+
if len(num_res_blocks) != len(channel_mult):
|
| 128 |
+
raise ValueError("provide num_res_blocks either as an int (globally constant) or "
|
| 129 |
+
"as a list/tuple (per-level) with the same length as channel_mult")
|
| 130 |
+
self.num_res_blocks = num_res_blocks
|
| 131 |
+
|
| 132 |
+
if disable_self_attentions is not None:
|
| 133 |
+
# should be a list of booleans, indicating whether to disable self-attention in TransformerBlocks or not
|
| 134 |
+
assert len(disable_self_attentions) == len(channel_mult)
|
| 135 |
+
if num_attention_blocks is not None:
|
| 136 |
+
assert len(num_attention_blocks) == len(self.num_res_blocks)
|
| 137 |
+
assert all(map(lambda i: self.num_res_blocks[i] >= num_attention_blocks[i], range(len(num_attention_blocks))))
|
| 138 |
+
|
| 139 |
+
transformer_depth = transformer_depth[:]
|
| 140 |
+
|
| 141 |
+
self.dropout = dropout
|
| 142 |
+
self.channel_mult = channel_mult
|
| 143 |
+
self.conv_resample = conv_resample
|
| 144 |
+
self.num_classes = num_classes
|
| 145 |
+
self.use_checkpoint = use_checkpoint
|
| 146 |
+
self.dtype = dtype
|
| 147 |
+
self.num_heads = num_heads
|
| 148 |
+
self.num_head_channels = num_head_channels
|
| 149 |
+
self.num_heads_upsample = num_heads_upsample
|
| 150 |
+
self.predict_codebook_ids = n_embed is not None
|
| 151 |
+
|
| 152 |
+
time_embed_dim = model_channels * 4
|
| 153 |
+
self.time_embed = nn.Sequential(
|
| 154 |
+
operations.Linear(model_channels, time_embed_dim, dtype=self.dtype, device=device),
|
| 155 |
+
nn.SiLU(),
|
| 156 |
+
operations.Linear(time_embed_dim, time_embed_dim, dtype=self.dtype, device=device),
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
if self.num_classes is not None:
|
| 160 |
+
if isinstance(self.num_classes, int):
|
| 161 |
+
self.label_emb = nn.Embedding(num_classes, time_embed_dim)
|
| 162 |
+
elif self.num_classes == "continuous":
|
| 163 |
+
self.label_emb = nn.Linear(1, time_embed_dim)
|
| 164 |
+
elif self.num_classes == "sequential":
|
| 165 |
+
assert adm_in_channels is not None
|
| 166 |
+
self.label_emb = nn.Sequential(
|
| 167 |
+
nn.Sequential(
|
| 168 |
+
operations.Linear(adm_in_channels, time_embed_dim, dtype=self.dtype, device=device),
|
| 169 |
+
nn.SiLU(),
|
| 170 |
+
operations.Linear(time_embed_dim, time_embed_dim, dtype=self.dtype, device=device),
|
| 171 |
+
)
|
| 172 |
+
)
|
| 173 |
+
else:
|
| 174 |
+
raise ValueError()
|
| 175 |
+
|
| 176 |
+
self.input_blocks = nn.ModuleList(
|
| 177 |
+
[
|
| 178 |
+
TimestepEmbedSequential(
|
| 179 |
+
operations.conv_nd(dims, in_channels, model_channels, 3, padding=1, dtype=self.dtype, device=device)
|
| 180 |
+
)
|
| 181 |
+
]
|
| 182 |
+
)
|
| 183 |
+
self.zero_convs = nn.ModuleList([self.make_zero_conv(model_channels, operations=operations, dtype=self.dtype, device=device)])
|
| 184 |
+
|
| 185 |
+
self.input_hint_block = TimestepEmbedSequential(
|
| 186 |
+
operations.conv_nd(dims, hint_channels, 16, 3, padding=1, dtype=self.dtype, device=device),
|
| 187 |
+
nn.SiLU(),
|
| 188 |
+
operations.conv_nd(dims, 16, 16, 3, padding=1, dtype=self.dtype, device=device),
|
| 189 |
+
nn.SiLU(),
|
| 190 |
+
operations.conv_nd(dims, 16, 32, 3, padding=1, stride=2, dtype=self.dtype, device=device),
|
| 191 |
+
nn.SiLU(),
|
| 192 |
+
operations.conv_nd(dims, 32, 32, 3, padding=1, dtype=self.dtype, device=device),
|
| 193 |
+
nn.SiLU(),
|
| 194 |
+
operations.conv_nd(dims, 32, 96, 3, padding=1, stride=2, dtype=self.dtype, device=device),
|
| 195 |
+
nn.SiLU(),
|
| 196 |
+
operations.conv_nd(dims, 96, 96, 3, padding=1, dtype=self.dtype, device=device),
|
| 197 |
+
nn.SiLU(),
|
| 198 |
+
operations.conv_nd(dims, 96, 256, 3, padding=1, stride=2, dtype=self.dtype, device=device),
|
| 199 |
+
nn.SiLU(),
|
| 200 |
+
operations.conv_nd(dims, 256, model_channels, 3, padding=1, dtype=self.dtype, device=device)
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
self._feature_size = model_channels
|
| 204 |
+
input_block_chans = [model_channels]
|
| 205 |
+
ch = model_channels
|
| 206 |
+
ds = 1
|
| 207 |
+
for level, mult in enumerate(channel_mult):
|
| 208 |
+
for nr in range(self.num_res_blocks[level]):
|
| 209 |
+
layers = [
|
| 210 |
+
ResBlock(
|
| 211 |
+
ch,
|
| 212 |
+
time_embed_dim,
|
| 213 |
+
dropout,
|
| 214 |
+
out_channels=mult * model_channels,
|
| 215 |
+
dims=dims,
|
| 216 |
+
use_checkpoint=use_checkpoint,
|
| 217 |
+
use_scale_shift_norm=use_scale_shift_norm,
|
| 218 |
+
dtype=self.dtype,
|
| 219 |
+
device=device,
|
| 220 |
+
operations=operations,
|
| 221 |
+
)
|
| 222 |
+
]
|
| 223 |
+
ch = mult * model_channels
|
| 224 |
+
num_transformers = transformer_depth.pop(0)
|
| 225 |
+
if num_transformers > 0:
|
| 226 |
+
if num_head_channels == -1:
|
| 227 |
+
dim_head = ch // num_heads
|
| 228 |
+
else:
|
| 229 |
+
num_heads = ch // num_head_channels
|
| 230 |
+
dim_head = num_head_channels
|
| 231 |
+
if legacy:
|
| 232 |
+
#num_heads = 1
|
| 233 |
+
dim_head = ch // num_heads if use_spatial_transformer else num_head_channels
|
| 234 |
+
if exists(disable_self_attentions):
|
| 235 |
+
disabled_sa = disable_self_attentions[level]
|
| 236 |
+
else:
|
| 237 |
+
disabled_sa = False
|
| 238 |
+
|
| 239 |
+
if not exists(num_attention_blocks) or nr < num_attention_blocks[level]:
|
| 240 |
+
layers.append(
|
| 241 |
+
SpatialTransformer(
|
| 242 |
+
ch, num_heads, dim_head, depth=num_transformers, context_dim=context_dim,
|
| 243 |
+
disable_self_attn=disabled_sa, use_linear=use_linear_in_transformer,
|
| 244 |
+
use_checkpoint=use_checkpoint, attn_precision=attn_precision, dtype=self.dtype, device=device, operations=operations
|
| 245 |
+
)
|
| 246 |
+
)
|
| 247 |
+
self.input_blocks.append(TimestepEmbedSequential(*layers))
|
| 248 |
+
self.zero_convs.append(self.make_zero_conv(ch, operations=operations, dtype=self.dtype, device=device))
|
| 249 |
+
self._feature_size += ch
|
| 250 |
+
input_block_chans.append(ch)
|
| 251 |
+
if level != len(channel_mult) - 1:
|
| 252 |
+
out_ch = ch
|
| 253 |
+
self.input_blocks.append(
|
| 254 |
+
TimestepEmbedSequential(
|
| 255 |
+
ResBlock(
|
| 256 |
+
ch,
|
| 257 |
+
time_embed_dim,
|
| 258 |
+
dropout,
|
| 259 |
+
out_channels=out_ch,
|
| 260 |
+
dims=dims,
|
| 261 |
+
use_checkpoint=use_checkpoint,
|
| 262 |
+
use_scale_shift_norm=use_scale_shift_norm,
|
| 263 |
+
down=True,
|
| 264 |
+
dtype=self.dtype,
|
| 265 |
+
device=device,
|
| 266 |
+
operations=operations
|
| 267 |
+
)
|
| 268 |
+
if resblock_updown
|
| 269 |
+
else Downsample(
|
| 270 |
+
ch, conv_resample, dims=dims, out_channels=out_ch, dtype=self.dtype, device=device, operations=operations
|
| 271 |
+
)
|
| 272 |
+
)
|
| 273 |
+
)
|
| 274 |
+
ch = out_ch
|
| 275 |
+
input_block_chans.append(ch)
|
| 276 |
+
self.zero_convs.append(self.make_zero_conv(ch, operations=operations, dtype=self.dtype, device=device))
|
| 277 |
+
ds *= 2
|
| 278 |
+
self._feature_size += ch
|
| 279 |
+
|
| 280 |
+
if num_head_channels == -1:
|
| 281 |
+
dim_head = ch // num_heads
|
| 282 |
+
else:
|
| 283 |
+
num_heads = ch // num_head_channels
|
| 284 |
+
dim_head = num_head_channels
|
| 285 |
+
if legacy:
|
| 286 |
+
#num_heads = 1
|
| 287 |
+
dim_head = ch // num_heads if use_spatial_transformer else num_head_channels
|
| 288 |
+
mid_block = [
|
| 289 |
+
ResBlock(
|
| 290 |
+
ch,
|
| 291 |
+
time_embed_dim,
|
| 292 |
+
dropout,
|
| 293 |
+
dims=dims,
|
| 294 |
+
use_checkpoint=use_checkpoint,
|
| 295 |
+
use_scale_shift_norm=use_scale_shift_norm,
|
| 296 |
+
dtype=self.dtype,
|
| 297 |
+
device=device,
|
| 298 |
+
operations=operations
|
| 299 |
+
)]
|
| 300 |
+
if transformer_depth_middle >= 0:
|
| 301 |
+
mid_block += [SpatialTransformer( # always uses a self-attn
|
| 302 |
+
ch, num_heads, dim_head, depth=transformer_depth_middle, context_dim=context_dim,
|
| 303 |
+
disable_self_attn=disable_middle_self_attn, use_linear=use_linear_in_transformer,
|
| 304 |
+
use_checkpoint=use_checkpoint, attn_precision=attn_precision, dtype=self.dtype, device=device, operations=operations
|
| 305 |
+
),
|
| 306 |
+
ResBlock(
|
| 307 |
+
ch,
|
| 308 |
+
time_embed_dim,
|
| 309 |
+
dropout,
|
| 310 |
+
dims=dims,
|
| 311 |
+
use_checkpoint=use_checkpoint,
|
| 312 |
+
use_scale_shift_norm=use_scale_shift_norm,
|
| 313 |
+
dtype=self.dtype,
|
| 314 |
+
device=device,
|
| 315 |
+
operations=operations
|
| 316 |
+
)]
|
| 317 |
+
self.middle_block = TimestepEmbedSequential(*mid_block)
|
| 318 |
+
self.middle_block_out = self.make_zero_conv(ch, operations=operations, dtype=self.dtype, device=device)
|
| 319 |
+
self._feature_size += ch
|
| 320 |
+
|
| 321 |
+
if union_controlnet_num_control_type is not None:
|
| 322 |
+
self.num_control_type = union_controlnet_num_control_type
|
| 323 |
+
num_trans_channel = 320
|
| 324 |
+
num_trans_head = 8
|
| 325 |
+
num_trans_layer = 1
|
| 326 |
+
num_proj_channel = 320
|
| 327 |
+
# task_scale_factor = num_trans_channel ** 0.5
|
| 328 |
+
self.task_embedding = nn.Parameter(torch.empty(self.num_control_type, num_trans_channel, dtype=self.dtype, device=device))
|
| 329 |
+
|
| 330 |
+
self.transformer_layes = nn.Sequential(*[ResBlockUnionControlnet(num_trans_channel, num_trans_head, dtype=self.dtype, device=device, operations=operations) for _ in range(num_trans_layer)])
|
| 331 |
+
self.spatial_ch_projs = operations.Linear(num_trans_channel, num_proj_channel, dtype=self.dtype, device=device)
|
| 332 |
+
#-----------------------------------------------------------------------------------------------------
|
| 333 |
+
|
| 334 |
+
control_add_embed_dim = 256
|
| 335 |
+
class ControlAddEmbedding(nn.Module):
|
| 336 |
+
def __init__(self, in_dim, out_dim, num_control_type, dtype=None, device=None, operations=None):
|
| 337 |
+
super().__init__()
|
| 338 |
+
self.num_control_type = num_control_type
|
| 339 |
+
self.in_dim = in_dim
|
| 340 |
+
self.linear_1 = operations.Linear(in_dim * num_control_type, out_dim, dtype=dtype, device=device)
|
| 341 |
+
self.linear_2 = operations.Linear(out_dim, out_dim, dtype=dtype, device=device)
|
| 342 |
+
def forward(self, control_type, dtype, device):
|
| 343 |
+
c_type = torch.zeros((self.num_control_type,), device=device)
|
| 344 |
+
c_type[control_type] = 1.0
|
| 345 |
+
c_type = timestep_embedding(c_type.flatten(), self.in_dim, repeat_only=False).to(dtype).reshape((-1, self.num_control_type * self.in_dim))
|
| 346 |
+
return self.linear_2(torch.nn.functional.silu(self.linear_1(c_type)))
|
| 347 |
+
|
| 348 |
+
self.control_add_embedding = ControlAddEmbedding(control_add_embed_dim, time_embed_dim, self.num_control_type, dtype=self.dtype, device=device, operations=operations)
|
| 349 |
+
else:
|
| 350 |
+
self.task_embedding = None
|
| 351 |
+
self.control_add_embedding = None
|
| 352 |
+
|
| 353 |
+
def union_controlnet_merge(self, hint, control_type, emb, context):
|
| 354 |
+
# Equivalent to: https://github.com/xinsir6/ControlNetPlus/tree/main
|
| 355 |
+
inputs = []
|
| 356 |
+
condition_list = []
|
| 357 |
+
|
| 358 |
+
for idx in range(min(1, len(control_type))):
|
| 359 |
+
controlnet_cond = self.input_hint_block(hint[idx], emb, context)
|
| 360 |
+
feat_seq = torch.mean(controlnet_cond, dim=(2, 3))
|
| 361 |
+
if idx < len(control_type):
|
| 362 |
+
feat_seq += self.task_embedding[control_type[idx]].to(dtype=feat_seq.dtype, device=feat_seq.device)
|
| 363 |
+
|
| 364 |
+
inputs.append(feat_seq.unsqueeze(1))
|
| 365 |
+
condition_list.append(controlnet_cond)
|
| 366 |
+
|
| 367 |
+
x = torch.cat(inputs, dim=1)
|
| 368 |
+
x = self.transformer_layes(x)
|
| 369 |
+
controlnet_cond_fuser = None
|
| 370 |
+
for idx in range(len(control_type)):
|
| 371 |
+
alpha = self.spatial_ch_projs(x[:, idx])
|
| 372 |
+
alpha = alpha.unsqueeze(-1).unsqueeze(-1)
|
| 373 |
+
o = condition_list[idx] + alpha
|
| 374 |
+
if controlnet_cond_fuser is None:
|
| 375 |
+
controlnet_cond_fuser = o
|
| 376 |
+
else:
|
| 377 |
+
controlnet_cond_fuser += o
|
| 378 |
+
return controlnet_cond_fuser
|
| 379 |
+
|
| 380 |
+
def make_zero_conv(self, channels, operations=None, dtype=None, device=None):
|
| 381 |
+
return TimestepEmbedSequential(operations.conv_nd(self.dims, channels, channels, 1, padding=0, dtype=dtype, device=device))
|
| 382 |
+
|
| 383 |
+
def forward(self, x, hint, timesteps, context, y=None, **kwargs):
|
| 384 |
+
t_emb = timestep_embedding(timesteps, self.model_channels, repeat_only=False).to(x.dtype)
|
| 385 |
+
emb = self.time_embed(t_emb)
|
| 386 |
+
|
| 387 |
+
guided_hint = None
|
| 388 |
+
if self.control_add_embedding is not None: #Union Controlnet
|
| 389 |
+
control_type = kwargs.get("control_type", [])
|
| 390 |
+
|
| 391 |
+
if any([c >= self.num_control_type for c in control_type]):
|
| 392 |
+
max_type = max(control_type)
|
| 393 |
+
max_type_name = {
|
| 394 |
+
v: k for k, v in UNION_CONTROLNET_TYPES.items()
|
| 395 |
+
}[max_type]
|
| 396 |
+
raise ValueError(
|
| 397 |
+
f"Control type {max_type_name}({max_type}) is out of range for the number of control types" +
|
| 398 |
+
f"({self.num_control_type}) supported.\n" +
|
| 399 |
+
"Please consider using the ProMax ControlNet Union model.\n" +
|
| 400 |
+
"https://huggingface.co/xinsir/controlnet-union-sdxl-1.0/tree/main"
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
emb += self.control_add_embedding(control_type, emb.dtype, emb.device)
|
| 404 |
+
if len(control_type) > 0:
|
| 405 |
+
if len(hint.shape) < 5:
|
| 406 |
+
hint = hint.unsqueeze(dim=0)
|
| 407 |
+
guided_hint = self.union_controlnet_merge(hint, control_type, emb, context)
|
| 408 |
+
|
| 409 |
+
if guided_hint is None:
|
| 410 |
+
guided_hint = self.input_hint_block(hint, emb, context)
|
| 411 |
+
|
| 412 |
+
out_output = []
|
| 413 |
+
out_middle = []
|
| 414 |
+
|
| 415 |
+
if self.num_classes is not None:
|
| 416 |
+
if y is None:
|
| 417 |
+
raise ValueError("y is None, did you try using a controlnet for SDXL on SD1?")
|
| 418 |
+
emb = emb + self.label_emb(y)
|
| 419 |
+
|
| 420 |
+
h = x
|
| 421 |
+
for module, zero_conv in zip(self.input_blocks, self.zero_convs):
|
| 422 |
+
if guided_hint is not None:
|
| 423 |
+
h = module(h, emb, context)
|
| 424 |
+
h += guided_hint
|
| 425 |
+
guided_hint = None
|
| 426 |
+
else:
|
| 427 |
+
h = module(h, emb, context)
|
| 428 |
+
out_output.append(zero_conv(h, emb, context))
|
| 429 |
+
|
| 430 |
+
h = self.middle_block(h, emb, context)
|
| 431 |
+
out_middle.append(self.middle_block_out(h, emb, context))
|
| 432 |
+
|
| 433 |
+
return {"middle": out_middle, "output": out_output}
|
| 434 |
+
|
comfy/cldm/control_types.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
UNION_CONTROLNET_TYPES = {
|
| 2 |
+
"openpose": 0,
|
| 3 |
+
"depth": 1,
|
| 4 |
+
"hed/pidi/scribble/ted": 2,
|
| 5 |
+
"canny/lineart/anime_lineart/mlsd": 3,
|
| 6 |
+
"normal": 4,
|
| 7 |
+
"segment": 5,
|
| 8 |
+
"tile": 6,
|
| 9 |
+
"repaint": 7,
|
| 10 |
+
}
|
comfy/cldm/dit_embedder.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from typing import List, Optional, Tuple
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from comfy.ldm.modules.diffusionmodules.mmdit import DismantledBlock, PatchEmbed, VectorEmbedder, TimestepEmbedder, get_2d_sincos_pos_embed_torch
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class ControlNetEmbedder(nn.Module):
|
| 12 |
+
|
| 13 |
+
def __init__(
|
| 14 |
+
self,
|
| 15 |
+
img_size: int,
|
| 16 |
+
patch_size: int,
|
| 17 |
+
in_chans: int,
|
| 18 |
+
attention_head_dim: int,
|
| 19 |
+
num_attention_heads: int,
|
| 20 |
+
adm_in_channels: int,
|
| 21 |
+
num_layers: int,
|
| 22 |
+
main_model_double: int,
|
| 23 |
+
double_y_emb: bool,
|
| 24 |
+
device: torch.device,
|
| 25 |
+
dtype: torch.dtype,
|
| 26 |
+
pos_embed_max_size: Optional[int] = None,
|
| 27 |
+
operations = None,
|
| 28 |
+
):
|
| 29 |
+
super().__init__()
|
| 30 |
+
self.main_model_double = main_model_double
|
| 31 |
+
self.dtype = dtype
|
| 32 |
+
self.hidden_size = num_attention_heads * attention_head_dim
|
| 33 |
+
self.patch_size = patch_size
|
| 34 |
+
self.x_embedder = PatchEmbed(
|
| 35 |
+
img_size=img_size,
|
| 36 |
+
patch_size=patch_size,
|
| 37 |
+
in_chans=in_chans,
|
| 38 |
+
embed_dim=self.hidden_size,
|
| 39 |
+
strict_img_size=pos_embed_max_size is None,
|
| 40 |
+
device=device,
|
| 41 |
+
dtype=dtype,
|
| 42 |
+
operations=operations,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
self.t_embedder = TimestepEmbedder(self.hidden_size, dtype=dtype, device=device, operations=operations)
|
| 46 |
+
|
| 47 |
+
self.double_y_emb = double_y_emb
|
| 48 |
+
if self.double_y_emb:
|
| 49 |
+
self.orig_y_embedder = VectorEmbedder(
|
| 50 |
+
adm_in_channels, self.hidden_size, dtype, device, operations=operations
|
| 51 |
+
)
|
| 52 |
+
self.y_embedder = VectorEmbedder(
|
| 53 |
+
self.hidden_size, self.hidden_size, dtype, device, operations=operations
|
| 54 |
+
)
|
| 55 |
+
else:
|
| 56 |
+
self.y_embedder = VectorEmbedder(
|
| 57 |
+
adm_in_channels, self.hidden_size, dtype, device, operations=operations
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
self.transformer_blocks = nn.ModuleList(
|
| 61 |
+
DismantledBlock(
|
| 62 |
+
hidden_size=self.hidden_size, num_heads=num_attention_heads, qkv_bias=True,
|
| 63 |
+
dtype=dtype, device=device, operations=operations
|
| 64 |
+
)
|
| 65 |
+
for _ in range(num_layers)
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# self.use_y_embedder = pooled_projection_dim != self.time_text_embed.text_embedder.linear_1.in_features
|
| 69 |
+
# TODO double check this logic when 8b
|
| 70 |
+
self.use_y_embedder = True
|
| 71 |
+
|
| 72 |
+
self.controlnet_blocks = nn.ModuleList([])
|
| 73 |
+
for _ in range(len(self.transformer_blocks)):
|
| 74 |
+
controlnet_block = operations.Linear(self.hidden_size, self.hidden_size, dtype=dtype, device=device)
|
| 75 |
+
self.controlnet_blocks.append(controlnet_block)
|
| 76 |
+
|
| 77 |
+
self.pos_embed_input = PatchEmbed(
|
| 78 |
+
img_size=img_size,
|
| 79 |
+
patch_size=patch_size,
|
| 80 |
+
in_chans=in_chans,
|
| 81 |
+
embed_dim=self.hidden_size,
|
| 82 |
+
strict_img_size=False,
|
| 83 |
+
device=device,
|
| 84 |
+
dtype=dtype,
|
| 85 |
+
operations=operations,
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
def forward(
|
| 89 |
+
self,
|
| 90 |
+
x: torch.Tensor,
|
| 91 |
+
timesteps: torch.Tensor,
|
| 92 |
+
y: Optional[torch.Tensor] = None,
|
| 93 |
+
context: Optional[torch.Tensor] = None,
|
| 94 |
+
hint = None,
|
| 95 |
+
) -> Tuple[Tensor, List[Tensor]]:
|
| 96 |
+
x_shape = list(x.shape)
|
| 97 |
+
x = self.x_embedder(x)
|
| 98 |
+
if not self.double_y_emb:
|
| 99 |
+
h = (x_shape[-2] + 1) // self.patch_size
|
| 100 |
+
w = (x_shape[-1] + 1) // self.patch_size
|
| 101 |
+
x += get_2d_sincos_pos_embed_torch(self.hidden_size, w, h, device=x.device)
|
| 102 |
+
c = self.t_embedder(timesteps, dtype=x.dtype)
|
| 103 |
+
if y is not None and self.y_embedder is not None:
|
| 104 |
+
if self.double_y_emb:
|
| 105 |
+
y = self.orig_y_embedder(y)
|
| 106 |
+
y = self.y_embedder(y)
|
| 107 |
+
c = c + y
|
| 108 |
+
|
| 109 |
+
x = x + self.pos_embed_input(hint)
|
| 110 |
+
|
| 111 |
+
block_out = ()
|
| 112 |
+
|
| 113 |
+
repeat = math.ceil(self.main_model_double / len(self.transformer_blocks))
|
| 114 |
+
for i in range(len(self.transformer_blocks)):
|
| 115 |
+
out = self.transformer_blocks[i](x, c)
|
| 116 |
+
if not self.double_y_emb:
|
| 117 |
+
x = out
|
| 118 |
+
block_out += (self.controlnet_blocks[i](out),) * repeat
|
| 119 |
+
|
| 120 |
+
return {"output": block_out}
|
comfy/cldm/mmdit.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from typing import Optional
|
| 3 |
+
import comfy.ldm.modules.diffusionmodules.mmdit
|
| 4 |
+
|
| 5 |
+
class ControlNet(comfy.ldm.modules.diffusionmodules.mmdit.MMDiT):
|
| 6 |
+
def __init__(
|
| 7 |
+
self,
|
| 8 |
+
num_blocks = None,
|
| 9 |
+
control_latent_channels = None,
|
| 10 |
+
dtype = None,
|
| 11 |
+
device = None,
|
| 12 |
+
operations = None,
|
| 13 |
+
**kwargs,
|
| 14 |
+
):
|
| 15 |
+
super().__init__(dtype=dtype, device=device, operations=operations, final_layer=False, num_blocks=num_blocks, **kwargs)
|
| 16 |
+
# controlnet_blocks
|
| 17 |
+
self.controlnet_blocks = torch.nn.ModuleList([])
|
| 18 |
+
for _ in range(len(self.joint_blocks)):
|
| 19 |
+
self.controlnet_blocks.append(operations.Linear(self.hidden_size, self.hidden_size, device=device, dtype=dtype))
|
| 20 |
+
|
| 21 |
+
if control_latent_channels is None:
|
| 22 |
+
control_latent_channels = self.in_channels
|
| 23 |
+
|
| 24 |
+
self.pos_embed_input = comfy.ldm.modules.diffusionmodules.mmdit.PatchEmbed(
|
| 25 |
+
None,
|
| 26 |
+
self.patch_size,
|
| 27 |
+
control_latent_channels,
|
| 28 |
+
self.hidden_size,
|
| 29 |
+
bias=True,
|
| 30 |
+
strict_img_size=False,
|
| 31 |
+
dtype=dtype,
|
| 32 |
+
device=device,
|
| 33 |
+
operations=operations
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
def forward(
|
| 37 |
+
self,
|
| 38 |
+
x: torch.Tensor,
|
| 39 |
+
timesteps: torch.Tensor,
|
| 40 |
+
y: Optional[torch.Tensor] = None,
|
| 41 |
+
context: Optional[torch.Tensor] = None,
|
| 42 |
+
hint = None,
|
| 43 |
+
) -> torch.Tensor:
|
| 44 |
+
|
| 45 |
+
#weird sd3 controlnet specific stuff
|
| 46 |
+
y = torch.zeros_like(y)
|
| 47 |
+
|
| 48 |
+
if self.context_processor is not None:
|
| 49 |
+
context = self.context_processor(context)
|
| 50 |
+
|
| 51 |
+
hw = x.shape[-2:]
|
| 52 |
+
x = self.x_embedder(x) + self.cropped_pos_embed(hw, device=x.device).to(dtype=x.dtype, device=x.device)
|
| 53 |
+
x += self.pos_embed_input(hint)
|
| 54 |
+
|
| 55 |
+
c = self.t_embedder(timesteps, dtype=x.dtype)
|
| 56 |
+
if y is not None and self.y_embedder is not None:
|
| 57 |
+
y = self.y_embedder(y)
|
| 58 |
+
c = c + y
|
| 59 |
+
|
| 60 |
+
if context is not None:
|
| 61 |
+
context = self.context_embedder(context)
|
| 62 |
+
|
| 63 |
+
output = []
|
| 64 |
+
|
| 65 |
+
blocks = len(self.joint_blocks)
|
| 66 |
+
for i in range(blocks):
|
| 67 |
+
context, x = self.joint_blocks[i](
|
| 68 |
+
context,
|
| 69 |
+
x,
|
| 70 |
+
c=c,
|
| 71 |
+
use_checkpoint=self.use_checkpoint,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
out = self.controlnet_blocks[i](x)
|
| 75 |
+
count = self.depth // blocks
|
| 76 |
+
if i == blocks - 1:
|
| 77 |
+
count -= 1
|
| 78 |
+
for j in range(count):
|
| 79 |
+
output.append(out)
|
| 80 |
+
|
| 81 |
+
return {"output": output}
|
comfy/comfy_types/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Comfy Typing
|
| 2 |
+
## Type hinting for ComfyUI Node development
|
| 3 |
+
|
| 4 |
+
This module provides type hinting and concrete convenience types for node developers.
|
| 5 |
+
If cloned to the custom_nodes directory of ComfyUI, types can be imported using:
|
| 6 |
+
|
| 7 |
+
```python
|
| 8 |
+
from comfy.comfy_types import IO, ComfyNodeABC, CheckLazyMixin
|
| 9 |
+
|
| 10 |
+
class ExampleNode(ComfyNodeABC):
|
| 11 |
+
@classmethod
|
| 12 |
+
def INPUT_TYPES(s) -> InputTypeDict:
|
| 13 |
+
return {"required": {}}
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
Full example is in [examples/example_nodes.py](examples/example_nodes.py).
|
| 17 |
+
|
| 18 |
+
# Types
|
| 19 |
+
A few primary types are documented below. More complete information is available via the docstrings on each type.
|
| 20 |
+
|
| 21 |
+
## `IO`
|
| 22 |
+
|
| 23 |
+
A string enum of built-in and a few custom data types. Includes the following special types and their requisite plumbing:
|
| 24 |
+
|
| 25 |
+
- `ANY`: `"*"`
|
| 26 |
+
- `NUMBER`: `"FLOAT,INT"`
|
| 27 |
+
- `PRIMITIVE`: `"STRING,FLOAT,INT,BOOLEAN"`
|
| 28 |
+
|
| 29 |
+
## `ComfyNodeABC`
|
| 30 |
+
|
| 31 |
+
An abstract base class for nodes, offering type-hinting / autocomplete, and somewhat-alright docstrings.
|
| 32 |
+
|
| 33 |
+
### Type hinting for `INPUT_TYPES`
|
| 34 |
+
|
| 35 |
+

|
| 36 |
+
|
| 37 |
+
### `INPUT_TYPES` return dict
|
| 38 |
+
|
| 39 |
+

|
| 40 |
+
|
| 41 |
+
### Options for individual inputs
|
| 42 |
+
|
| 43 |
+

|
comfy/comfy_types/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from typing import Callable, Protocol, TypedDict, Optional, List
|
| 3 |
+
from .node_typing import IO, InputTypeDict, ComfyNodeABC, CheckLazyMixin, FileLocator
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class UnetApplyFunction(Protocol):
|
| 7 |
+
"""Function signature protocol on comfy.model_base.BaseModel.apply_model"""
|
| 8 |
+
|
| 9 |
+
def __call__(self, x: torch.Tensor, t: torch.Tensor, **kwargs) -> torch.Tensor:
|
| 10 |
+
pass
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class UnetApplyConds(TypedDict):
|
| 14 |
+
"""Optional conditions for unet apply function."""
|
| 15 |
+
|
| 16 |
+
c_concat: Optional[torch.Tensor]
|
| 17 |
+
c_crossattn: Optional[torch.Tensor]
|
| 18 |
+
control: Optional[torch.Tensor]
|
| 19 |
+
transformer_options: Optional[dict]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class UnetParams(TypedDict):
|
| 23 |
+
# Tensor of shape [B, C, H, W]
|
| 24 |
+
input: torch.Tensor
|
| 25 |
+
# Tensor of shape [B]
|
| 26 |
+
timestep: torch.Tensor
|
| 27 |
+
c: UnetApplyConds
|
| 28 |
+
# List of [0, 1], [0], [1], ...
|
| 29 |
+
# 0 means conditional, 1 means conditional unconditional
|
| 30 |
+
cond_or_uncond: List[int]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
UnetWrapperFunction = Callable[[UnetApplyFunction, UnetParams], torch.Tensor]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
__all__ = [
|
| 37 |
+
"UnetWrapperFunction",
|
| 38 |
+
UnetApplyConds.__name__,
|
| 39 |
+
UnetParams.__name__,
|
| 40 |
+
UnetApplyFunction.__name__,
|
| 41 |
+
IO.__name__,
|
| 42 |
+
InputTypeDict.__name__,
|
| 43 |
+
ComfyNodeABC.__name__,
|
| 44 |
+
CheckLazyMixin.__name__,
|
| 45 |
+
FileLocator.__name__,
|
| 46 |
+
]
|
comfy/comfy_types/examples/example_nodes.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict
|
| 2 |
+
from inspect import cleandoc
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class ExampleNode(ComfyNodeABC):
|
| 6 |
+
"""An example node that just adds 1 to an input integer.
|
| 7 |
+
|
| 8 |
+
* Requires a modern IDE to provide any benefit (detail: an IDE configured with analysis paths etc).
|
| 9 |
+
* This node is intended as an example for developers only.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
DESCRIPTION = cleandoc(__doc__)
|
| 13 |
+
CATEGORY = "examples"
|
| 14 |
+
|
| 15 |
+
@classmethod
|
| 16 |
+
def INPUT_TYPES(s) -> InputTypeDict:
|
| 17 |
+
return {
|
| 18 |
+
"required": {
|
| 19 |
+
"input_int": (IO.INT, {"defaultInput": True}),
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
RETURN_TYPES = (IO.INT,)
|
| 24 |
+
RETURN_NAMES = ("input_plus_one",)
|
| 25 |
+
FUNCTION = "execute"
|
| 26 |
+
|
| 27 |
+
def execute(self, input_int: int):
|
| 28 |
+
return (input_int + 1,)
|
comfy/comfy_types/node_typing.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Comfy-specific type hinting"""
|
| 2 |
+
|
| 3 |
+
from typing import Literal, TypedDict, Optional
|
| 4 |
+
from typing_extensions import NotRequired
|
| 5 |
+
from abc import ABC, abstractmethod
|
| 6 |
+
from enum import Enum
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class StrEnum(str, Enum):
|
| 10 |
+
"""Base class for string enums. Python's StrEnum is not available until 3.11."""
|
| 11 |
+
|
| 12 |
+
def __str__(self) -> str:
|
| 13 |
+
return self.value
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class IO(StrEnum):
|
| 17 |
+
"""Node input/output data types.
|
| 18 |
+
|
| 19 |
+
Includes functionality for ``"*"`` (`ANY`) and ``"MULTI,TYPES"``.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
STRING = "STRING"
|
| 23 |
+
IMAGE = "IMAGE"
|
| 24 |
+
MASK = "MASK"
|
| 25 |
+
LATENT = "LATENT"
|
| 26 |
+
BOOLEAN = "BOOLEAN"
|
| 27 |
+
INT = "INT"
|
| 28 |
+
FLOAT = "FLOAT"
|
| 29 |
+
COMBO = "COMBO"
|
| 30 |
+
CONDITIONING = "CONDITIONING"
|
| 31 |
+
SAMPLER = "SAMPLER"
|
| 32 |
+
SIGMAS = "SIGMAS"
|
| 33 |
+
GUIDER = "GUIDER"
|
| 34 |
+
NOISE = "NOISE"
|
| 35 |
+
CLIP = "CLIP"
|
| 36 |
+
CONTROL_NET = "CONTROL_NET"
|
| 37 |
+
VAE = "VAE"
|
| 38 |
+
MODEL = "MODEL"
|
| 39 |
+
LORA_MODEL = "LORA_MODEL"
|
| 40 |
+
LOSS_MAP = "LOSS_MAP"
|
| 41 |
+
CLIP_VISION = "CLIP_VISION"
|
| 42 |
+
CLIP_VISION_OUTPUT = "CLIP_VISION_OUTPUT"
|
| 43 |
+
STYLE_MODEL = "STYLE_MODEL"
|
| 44 |
+
GLIGEN = "GLIGEN"
|
| 45 |
+
UPSCALE_MODEL = "UPSCALE_MODEL"
|
| 46 |
+
AUDIO = "AUDIO"
|
| 47 |
+
WEBCAM = "WEBCAM"
|
| 48 |
+
POINT = "POINT"
|
| 49 |
+
FACE_ANALYSIS = "FACE_ANALYSIS"
|
| 50 |
+
BBOX = "BBOX"
|
| 51 |
+
SEGS = "SEGS"
|
| 52 |
+
VIDEO = "VIDEO"
|
| 53 |
+
|
| 54 |
+
ANY = "*"
|
| 55 |
+
"""Always matches any type, but at a price.
|
| 56 |
+
|
| 57 |
+
Causes some functionality issues (e.g. reroutes, link types), and should be avoided whenever possible.
|
| 58 |
+
"""
|
| 59 |
+
NUMBER = "FLOAT,INT"
|
| 60 |
+
"""A float or an int - could be either"""
|
| 61 |
+
PRIMITIVE = "STRING,FLOAT,INT,BOOLEAN"
|
| 62 |
+
"""Could be any of: string, float, int, or bool"""
|
| 63 |
+
|
| 64 |
+
def __ne__(self, value: object) -> bool:
|
| 65 |
+
if self == "*" or value == "*":
|
| 66 |
+
return False
|
| 67 |
+
if not isinstance(value, str):
|
| 68 |
+
return True
|
| 69 |
+
a = frozenset(self.split(","))
|
| 70 |
+
b = frozenset(value.split(","))
|
| 71 |
+
return not (b.issubset(a) or a.issubset(b))
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class RemoteInputOptions(TypedDict):
|
| 75 |
+
route: str
|
| 76 |
+
"""The route to the remote source."""
|
| 77 |
+
refresh_button: bool
|
| 78 |
+
"""Specifies whether to show a refresh button in the UI below the widget."""
|
| 79 |
+
control_after_refresh: Literal["first", "last"]
|
| 80 |
+
"""Specifies the control after the refresh button is clicked. If "first", the first item will be automatically selected, and so on."""
|
| 81 |
+
timeout: int
|
| 82 |
+
"""The maximum amount of time to wait for a response from the remote source in milliseconds."""
|
| 83 |
+
max_retries: int
|
| 84 |
+
"""The maximum number of retries before aborting the request."""
|
| 85 |
+
refresh: int
|
| 86 |
+
"""The TTL of the remote input's value in milliseconds. Specifies the interval at which the remote input's value is refreshed."""
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class MultiSelectOptions(TypedDict):
|
| 90 |
+
placeholder: NotRequired[str]
|
| 91 |
+
"""The placeholder text to display in the multi-select widget when no items are selected."""
|
| 92 |
+
chip: NotRequired[bool]
|
| 93 |
+
"""Specifies whether to use chips instead of comma separated values for the multi-select widget."""
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class InputTypeOptions(TypedDict):
|
| 97 |
+
"""Provides type hinting for the return type of the INPUT_TYPES node function.
|
| 98 |
+
|
| 99 |
+
Due to IDE limitations with unions, for now all options are available for all types (e.g. `label_on` is hinted even when the type is not `IO.BOOLEAN`).
|
| 100 |
+
|
| 101 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/datatypes
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
default: NotRequired[bool | str | float | int | list | tuple]
|
| 105 |
+
"""The default value of the widget"""
|
| 106 |
+
defaultInput: NotRequired[bool]
|
| 107 |
+
"""@deprecated in v1.16 frontend. v1.16 frontend allows input socket and widget to co-exist.
|
| 108 |
+
- defaultInput on required inputs should be dropped.
|
| 109 |
+
- defaultInput on optional inputs should be replaced with forceInput.
|
| 110 |
+
Ref: https://github.com/Comfy-Org/ComfyUI_frontend/pull/3364
|
| 111 |
+
"""
|
| 112 |
+
forceInput: NotRequired[bool]
|
| 113 |
+
"""Forces the input to be an input slot rather than a widget even a widget is available for the input type."""
|
| 114 |
+
lazy: NotRequired[bool]
|
| 115 |
+
"""Declares that this input uses lazy evaluation"""
|
| 116 |
+
rawLink: NotRequired[bool]
|
| 117 |
+
"""When a link exists, rather than receiving the evaluated value, you will receive the link (i.e. `["nodeId", <outputIndex>]`). Designed for node expansion."""
|
| 118 |
+
tooltip: NotRequired[str]
|
| 119 |
+
"""Tooltip for the input (or widget), shown on pointer hover"""
|
| 120 |
+
socketless: NotRequired[bool]
|
| 121 |
+
"""All inputs (including widgets) have an input socket to connect links. When ``true``, if there is a widget for this input, no socket will be created.
|
| 122 |
+
Available from frontend v1.17.5
|
| 123 |
+
Ref: https://github.com/Comfy-Org/ComfyUI_frontend/pull/3548
|
| 124 |
+
"""
|
| 125 |
+
widgetType: NotRequired[str]
|
| 126 |
+
"""Specifies a type to be used for widget initialization if different from the input type.
|
| 127 |
+
Available from frontend v1.18.0
|
| 128 |
+
https://github.com/Comfy-Org/ComfyUI_frontend/pull/3550"""
|
| 129 |
+
# class InputTypeNumber(InputTypeOptions):
|
| 130 |
+
# default: float | int
|
| 131 |
+
min: NotRequired[float]
|
| 132 |
+
"""The minimum value of a number (``FLOAT`` | ``INT``)"""
|
| 133 |
+
max: NotRequired[float]
|
| 134 |
+
"""The maximum value of a number (``FLOAT`` | ``INT``)"""
|
| 135 |
+
step: NotRequired[float]
|
| 136 |
+
"""The amount to increment or decrement a widget by when stepping up/down (``FLOAT`` | ``INT``)"""
|
| 137 |
+
round: NotRequired[float]
|
| 138 |
+
"""Floats are rounded by this value (``FLOAT``)"""
|
| 139 |
+
# class InputTypeBoolean(InputTypeOptions):
|
| 140 |
+
# default: bool
|
| 141 |
+
label_on: NotRequired[str]
|
| 142 |
+
"""The label to use in the UI when the bool is True (``BOOLEAN``)"""
|
| 143 |
+
label_off: NotRequired[str]
|
| 144 |
+
"""The label to use in the UI when the bool is False (``BOOLEAN``)"""
|
| 145 |
+
# class InputTypeString(InputTypeOptions):
|
| 146 |
+
# default: str
|
| 147 |
+
multiline: NotRequired[bool]
|
| 148 |
+
"""Use a multiline text box (``STRING``)"""
|
| 149 |
+
placeholder: NotRequired[str]
|
| 150 |
+
"""Placeholder text to display in the UI when empty (``STRING``)"""
|
| 151 |
+
# Deprecated:
|
| 152 |
+
# defaultVal: str
|
| 153 |
+
dynamicPrompts: NotRequired[bool]
|
| 154 |
+
"""Causes the front-end to evaluate dynamic prompts (``STRING``)"""
|
| 155 |
+
# class InputTypeCombo(InputTypeOptions):
|
| 156 |
+
image_upload: NotRequired[bool]
|
| 157 |
+
"""Specifies whether the input should have an image upload button and image preview attached to it. Requires that the input's name is `image`."""
|
| 158 |
+
image_folder: NotRequired[Literal["input", "output", "temp"]]
|
| 159 |
+
"""Specifies which folder to get preview images from if the input has the ``image_upload`` flag.
|
| 160 |
+
"""
|
| 161 |
+
remote: NotRequired[RemoteInputOptions]
|
| 162 |
+
"""Specifies the configuration for a remote input.
|
| 163 |
+
Available after ComfyUI frontend v1.9.7
|
| 164 |
+
https://github.com/Comfy-Org/ComfyUI_frontend/pull/2422"""
|
| 165 |
+
control_after_generate: NotRequired[bool]
|
| 166 |
+
"""Specifies whether a control widget should be added to the input, adding options to automatically change the value after each prompt is queued. Currently only used for INT and COMBO types."""
|
| 167 |
+
options: NotRequired[list[str | int | float]]
|
| 168 |
+
"""COMBO type only. Specifies the selectable options for the combo widget.
|
| 169 |
+
Prefer:
|
| 170 |
+
["COMBO", {"options": ["Option 1", "Option 2", "Option 3"]}]
|
| 171 |
+
Over:
|
| 172 |
+
[["Option 1", "Option 2", "Option 3"]]
|
| 173 |
+
"""
|
| 174 |
+
multi_select: NotRequired[MultiSelectOptions]
|
| 175 |
+
"""COMBO type only. Specifies the configuration for a multi-select widget.
|
| 176 |
+
Available after ComfyUI frontend v1.13.4
|
| 177 |
+
https://github.com/Comfy-Org/ComfyUI_frontend/pull/2987"""
|
| 178 |
+
gradient_stops: NotRequired[list[dict]]
|
| 179 |
+
"""Gradient color stops for gradientslider display mode. Each stop is {"offset": float, "color": [r, g, b]}."""
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class HiddenInputTypeDict(TypedDict):
|
| 183 |
+
"""Provides type hinting for the hidden entry of node INPUT_TYPES."""
|
| 184 |
+
|
| 185 |
+
node_id: NotRequired[Literal["UNIQUE_ID"]]
|
| 186 |
+
"""UNIQUE_ID is the unique identifier of the node, and matches the id property of the node on the client side. It is commonly used in client-server communications (see messages)."""
|
| 187 |
+
unique_id: NotRequired[Literal["UNIQUE_ID"]]
|
| 188 |
+
"""UNIQUE_ID is the unique identifier of the node, and matches the id property of the node on the client side. It is commonly used in client-server communications (see messages)."""
|
| 189 |
+
prompt: NotRequired[Literal["PROMPT"]]
|
| 190 |
+
"""PROMPT is the complete prompt sent by the client to the server. See the prompt object for a full description."""
|
| 191 |
+
extra_pnginfo: NotRequired[Literal["EXTRA_PNGINFO"]]
|
| 192 |
+
"""EXTRA_PNGINFO is a dictionary that will be copied into the metadata of any .png files saved. Custom nodes can store additional information in this dictionary for saving (or as a way to communicate with a downstream node)."""
|
| 193 |
+
dynprompt: NotRequired[Literal["DYNPROMPT"]]
|
| 194 |
+
"""DYNPROMPT is an instance of comfy_execution.graph.DynamicPrompt. It differs from PROMPT in that it may mutate during the course of execution in response to Node Expansion."""
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class InputTypeDict(TypedDict):
|
| 198 |
+
"""Provides type hinting for node INPUT_TYPES.
|
| 199 |
+
|
| 200 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/more_on_inputs
|
| 201 |
+
"""
|
| 202 |
+
|
| 203 |
+
required: NotRequired[dict[str, tuple[IO, InputTypeOptions]]]
|
| 204 |
+
"""Describes all inputs that must be connected for the node to execute."""
|
| 205 |
+
optional: NotRequired[dict[str, tuple[IO, InputTypeOptions]]]
|
| 206 |
+
"""Describes inputs which do not need to be connected."""
|
| 207 |
+
hidden: NotRequired[HiddenInputTypeDict]
|
| 208 |
+
"""Offers advanced functionality and server-client communication.
|
| 209 |
+
|
| 210 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/more_on_inputs#hidden-inputs
|
| 211 |
+
"""
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
class ComfyNodeABC(ABC):
|
| 215 |
+
"""Abstract base class for Comfy nodes. Includes the names and expected types of attributes.
|
| 216 |
+
|
| 217 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/server_overview
|
| 218 |
+
"""
|
| 219 |
+
|
| 220 |
+
DESCRIPTION: str
|
| 221 |
+
"""Node description, shown as a tooltip when hovering over the node.
|
| 222 |
+
|
| 223 |
+
Usage::
|
| 224 |
+
|
| 225 |
+
# Explicitly define the description
|
| 226 |
+
DESCRIPTION = "Example description here."
|
| 227 |
+
|
| 228 |
+
# Use the docstring of the node class.
|
| 229 |
+
DESCRIPTION = cleandoc(__doc__)
|
| 230 |
+
"""
|
| 231 |
+
CATEGORY: str
|
| 232 |
+
"""The category of the node, as per the "Add Node" menu.
|
| 233 |
+
|
| 234 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/server_overview#category
|
| 235 |
+
"""
|
| 236 |
+
EXPERIMENTAL: bool
|
| 237 |
+
"""Flags a node as experimental, informing users that it may change or not work as expected."""
|
| 238 |
+
DEPRECATED: bool
|
| 239 |
+
"""Flags a node as deprecated, indicating to users that they should find alternatives to this node."""
|
| 240 |
+
DEV_ONLY: bool
|
| 241 |
+
"""Flags a node as dev-only, hiding it from search/menus unless dev mode is enabled."""
|
| 242 |
+
API_NODE: Optional[bool]
|
| 243 |
+
"""Flags a node as an API node. See: https://docs.comfy.org/tutorials/api-nodes/overview."""
|
| 244 |
+
|
| 245 |
+
@classmethod
|
| 246 |
+
@abstractmethod
|
| 247 |
+
def INPUT_TYPES(s) -> InputTypeDict:
|
| 248 |
+
"""Defines node inputs.
|
| 249 |
+
|
| 250 |
+
* Must include the ``required`` key, which describes all inputs that must be connected for the node to execute.
|
| 251 |
+
* The ``optional`` key can be added to describe inputs which do not need to be connected.
|
| 252 |
+
* The ``hidden`` key offers some advanced functionality. More info at: https://docs.comfy.org/custom-nodes/backend/more_on_inputs#hidden-inputs
|
| 253 |
+
|
| 254 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/server_overview#input-types
|
| 255 |
+
"""
|
| 256 |
+
return {"required": {}}
|
| 257 |
+
|
| 258 |
+
OUTPUT_NODE: bool
|
| 259 |
+
"""Flags this node as an output node, causing any inputs it requires to be executed.
|
| 260 |
+
|
| 261 |
+
If a node is not connected to any output nodes, that node will not be executed. Usage::
|
| 262 |
+
|
| 263 |
+
OUTPUT_NODE = True
|
| 264 |
+
|
| 265 |
+
From the docs:
|
| 266 |
+
|
| 267 |
+
By default, a node is not considered an output. Set ``OUTPUT_NODE = True`` to specify that it is.
|
| 268 |
+
|
| 269 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/server_overview#output-node
|
| 270 |
+
"""
|
| 271 |
+
INPUT_IS_LIST: bool
|
| 272 |
+
"""A flag indicating if this node implements the additional code necessary to deal with OUTPUT_IS_LIST nodes.
|
| 273 |
+
|
| 274 |
+
All inputs of ``type`` will become ``list[type]``, regardless of how many items are passed in. This also affects ``check_lazy_status``.
|
| 275 |
+
|
| 276 |
+
From the docs:
|
| 277 |
+
|
| 278 |
+
A node can also override the default input behaviour and receive the whole list in a single call. This is done by setting a class attribute `INPUT_IS_LIST` to ``True``.
|
| 279 |
+
|
| 280 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/lists#list-processing
|
| 281 |
+
"""
|
| 282 |
+
OUTPUT_IS_LIST: tuple[bool, ...]
|
| 283 |
+
"""A tuple indicating which node outputs are lists, but will be connected to nodes that expect individual items.
|
| 284 |
+
|
| 285 |
+
Connected nodes that do not implement `INPUT_IS_LIST` will be executed once for every item in the list.
|
| 286 |
+
|
| 287 |
+
A ``tuple[bool]``, where the items match those in `RETURN_TYPES`::
|
| 288 |
+
|
| 289 |
+
RETURN_TYPES = (IO.INT, IO.INT, IO.STRING)
|
| 290 |
+
OUTPUT_IS_LIST = (True, True, False) # The string output will be handled normally
|
| 291 |
+
|
| 292 |
+
From the docs:
|
| 293 |
+
|
| 294 |
+
In order to tell Comfy that the list being returned should not be wrapped, but treated as a series of data for sequential processing,
|
| 295 |
+
the node should provide a class attribute `OUTPUT_IS_LIST`, which is a ``tuple[bool]``, of the same length as `RETURN_TYPES`,
|
| 296 |
+
specifying which outputs which should be so treated.
|
| 297 |
+
|
| 298 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/lists#list-processing
|
| 299 |
+
"""
|
| 300 |
+
|
| 301 |
+
RETURN_TYPES: tuple[IO, ...]
|
| 302 |
+
"""A tuple representing the outputs of this node.
|
| 303 |
+
|
| 304 |
+
Usage::
|
| 305 |
+
|
| 306 |
+
RETURN_TYPES = (IO.INT, "INT", "CUSTOM_TYPE")
|
| 307 |
+
|
| 308 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/server_overview#return-types
|
| 309 |
+
"""
|
| 310 |
+
RETURN_NAMES: tuple[str, ...]
|
| 311 |
+
"""The output slot names for each item in `RETURN_TYPES`, e.g. ``RETURN_NAMES = ("count", "filter_string")``
|
| 312 |
+
|
| 313 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/server_overview#return-names
|
| 314 |
+
"""
|
| 315 |
+
OUTPUT_TOOLTIPS: tuple[str, ...]
|
| 316 |
+
"""A tuple of strings to use as tooltips for node outputs, one for each item in `RETURN_TYPES`."""
|
| 317 |
+
FUNCTION: str
|
| 318 |
+
"""The name of the function to execute as a literal string, e.g. `FUNCTION = "execute"`
|
| 319 |
+
|
| 320 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/server_overview#function
|
| 321 |
+
"""
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
class CheckLazyMixin:
|
| 325 |
+
"""Provides a basic check_lazy_status implementation and type hinting for nodes that use lazy inputs."""
|
| 326 |
+
|
| 327 |
+
def check_lazy_status(self, **kwargs) -> list[str]:
|
| 328 |
+
"""Returns a list of input names that should be evaluated.
|
| 329 |
+
|
| 330 |
+
This basic mixin impl. requires all inputs.
|
| 331 |
+
|
| 332 |
+
:kwargs: All node inputs will be included here. If the input is ``None``, it should be assumed that it has not yet been evaluated. \
|
| 333 |
+
When using ``INPUT_IS_LIST = True``, unevaluated will instead be ``(None,)``.
|
| 334 |
+
|
| 335 |
+
Params should match the nodes execution ``FUNCTION`` (self, and all inputs by name).
|
| 336 |
+
Will be executed repeatedly until it returns an empty list, or all requested items were already evaluated (and sent as params).
|
| 337 |
+
|
| 338 |
+
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/lazy_evaluation#defining-check-lazy-status
|
| 339 |
+
"""
|
| 340 |
+
|
| 341 |
+
need = [name for name in kwargs if kwargs[name] is None]
|
| 342 |
+
return need
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
class FileLocator(TypedDict):
|
| 346 |
+
"""Provides type hinting for the file location"""
|
| 347 |
+
|
| 348 |
+
filename: str
|
| 349 |
+
"""The filename of the file."""
|
| 350 |
+
subfolder: str
|
| 351 |
+
"""The subfolder of the file."""
|
| 352 |
+
type: Literal["input", "output", "temp"]
|
| 353 |
+
"""The root folder of the file."""
|
comfy/diffusers_load.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
import comfy.sd
|
| 4 |
+
|
| 5 |
+
def first_file(path, filenames):
|
| 6 |
+
for f in filenames:
|
| 7 |
+
p = os.path.join(path, f)
|
| 8 |
+
if os.path.exists(p):
|
| 9 |
+
return p
|
| 10 |
+
return None
|
| 11 |
+
|
| 12 |
+
def load_diffusers(model_path, output_vae=True, output_clip=True, embedding_directory=None):
|
| 13 |
+
diffusion_model_names = ["diffusion_pytorch_model.fp16.safetensors", "diffusion_pytorch_model.safetensors", "diffusion_pytorch_model.fp16.bin", "diffusion_pytorch_model.bin"]
|
| 14 |
+
unet_path = first_file(os.path.join(model_path, "unet"), diffusion_model_names)
|
| 15 |
+
vae_path = first_file(os.path.join(model_path, "vae"), diffusion_model_names)
|
| 16 |
+
|
| 17 |
+
text_encoder_model_names = ["model.fp16.safetensors", "model.safetensors", "pytorch_model.fp16.bin", "pytorch_model.bin"]
|
| 18 |
+
text_encoder1_path = first_file(os.path.join(model_path, "text_encoder"), text_encoder_model_names)
|
| 19 |
+
text_encoder2_path = first_file(os.path.join(model_path, "text_encoder_2"), text_encoder_model_names)
|
| 20 |
+
|
| 21 |
+
text_encoder_paths = [text_encoder1_path]
|
| 22 |
+
if text_encoder2_path is not None:
|
| 23 |
+
text_encoder_paths.append(text_encoder2_path)
|
| 24 |
+
|
| 25 |
+
unet = comfy.sd.load_diffusion_model(unet_path)
|
| 26 |
+
|
| 27 |
+
clip = None
|
| 28 |
+
if output_clip:
|
| 29 |
+
clip = comfy.sd.load_clip(text_encoder_paths, embedding_directory=embedding_directory)
|
| 30 |
+
|
| 31 |
+
vae = None
|
| 32 |
+
if output_vae:
|
| 33 |
+
sd = comfy.utils.load_torch_file(vae_path)
|
| 34 |
+
vae = comfy.sd.VAE(sd=sd)
|
| 35 |
+
|
| 36 |
+
return (unet, clip, vae)
|
comfy/extra_samplers/uni_pc.py
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#code taken from: https://github.com/wl-zhao/UniPC and modified
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import math
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
from tqdm.auto import trange
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class NoiseScheduleVP:
|
| 11 |
+
def __init__(
|
| 12 |
+
self,
|
| 13 |
+
schedule='discrete',
|
| 14 |
+
betas=None,
|
| 15 |
+
alphas_cumprod=None,
|
| 16 |
+
continuous_beta_0=0.1,
|
| 17 |
+
continuous_beta_1=20.,
|
| 18 |
+
):
|
| 19 |
+
r"""Create a wrapper class for the forward SDE (VP type).
|
| 20 |
+
|
| 21 |
+
***
|
| 22 |
+
Update: We support discrete-time diffusion models by implementing a picewise linear interpolation for log_alpha_t.
|
| 23 |
+
We recommend to use schedule='discrete' for the discrete-time diffusion models, especially for high-resolution images.
|
| 24 |
+
***
|
| 25 |
+
|
| 26 |
+
The forward SDE ensures that the condition distribution q_{t|0}(x_t | x_0) = N ( alpha_t * x_0, sigma_t^2 * I ).
|
| 27 |
+
We further define lambda_t = log(alpha_t) - log(sigma_t), which is the half-logSNR (described in the DPM-Solver paper).
|
| 28 |
+
Therefore, we implement the functions for computing alpha_t, sigma_t and lambda_t. For t in [0, T], we have:
|
| 29 |
+
|
| 30 |
+
log_alpha_t = self.marginal_log_mean_coeff(t)
|
| 31 |
+
sigma_t = self.marginal_std(t)
|
| 32 |
+
lambda_t = self.marginal_lambda(t)
|
| 33 |
+
|
| 34 |
+
Moreover, as lambda(t) is an invertible function, we also support its inverse function:
|
| 35 |
+
|
| 36 |
+
t = self.inverse_lambda(lambda_t)
|
| 37 |
+
|
| 38 |
+
===============================================================
|
| 39 |
+
|
| 40 |
+
We support both discrete-time DPMs (trained on n = 0, 1, ..., N-1) and continuous-time DPMs (trained on t in [t_0, T]).
|
| 41 |
+
|
| 42 |
+
1. For discrete-time DPMs:
|
| 43 |
+
|
| 44 |
+
For discrete-time DPMs trained on n = 0, 1, ..., N-1, we convert the discrete steps to continuous time steps by:
|
| 45 |
+
t_i = (i + 1) / N
|
| 46 |
+
e.g. for N = 1000, we have t_0 = 1e-3 and T = t_{N-1} = 1.
|
| 47 |
+
We solve the corresponding diffusion ODE from time T = 1 to time t_0 = 1e-3.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
betas: A `torch.Tensor`. The beta array for the discrete-time DPM. (See the original DDPM paper for details)
|
| 51 |
+
alphas_cumprod: A `torch.Tensor`. The cumprod alphas for the discrete-time DPM. (See the original DDPM paper for details)
|
| 52 |
+
|
| 53 |
+
Note that we always have alphas_cumprod = cumprod(betas). Therefore, we only need to set one of `betas` and `alphas_cumprod`.
|
| 54 |
+
|
| 55 |
+
**Important**: Please pay special attention for the args for `alphas_cumprod`:
|
| 56 |
+
The `alphas_cumprod` is the \hat{alpha_n} arrays in the notations of DDPM. Specifically, DDPMs assume that
|
| 57 |
+
q_{t_n | 0}(x_{t_n} | x_0) = N ( \sqrt{\hat{alpha_n}} * x_0, (1 - \hat{alpha_n}) * I ).
|
| 58 |
+
Therefore, the notation \hat{alpha_n} is different from the notation alpha_t in DPM-Solver. In fact, we have
|
| 59 |
+
alpha_{t_n} = \sqrt{\hat{alpha_n}},
|
| 60 |
+
and
|
| 61 |
+
log(alpha_{t_n}) = 0.5 * log(\hat{alpha_n}).
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
2. For continuous-time DPMs:
|
| 65 |
+
|
| 66 |
+
We support two types of VPSDEs: linear (DDPM) and cosine (improved-DDPM). The hyperparameters for the noise
|
| 67 |
+
schedule are the default settings in DDPM and improved-DDPM:
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
beta_min: A `float` number. The smallest beta for the linear schedule.
|
| 71 |
+
beta_max: A `float` number. The largest beta for the linear schedule.
|
| 72 |
+
cosine_s: A `float` number. The hyperparameter in the cosine schedule.
|
| 73 |
+
cosine_beta_max: A `float` number. The hyperparameter in the cosine schedule.
|
| 74 |
+
T: A `float` number. The ending time of the forward process.
|
| 75 |
+
|
| 76 |
+
===============================================================
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
schedule: A `str`. The noise schedule of the forward SDE. 'discrete' for discrete-time DPMs,
|
| 80 |
+
'linear' or 'cosine' for continuous-time DPMs.
|
| 81 |
+
Returns:
|
| 82 |
+
A wrapper object of the forward SDE (VP type).
|
| 83 |
+
|
| 84 |
+
===============================================================
|
| 85 |
+
|
| 86 |
+
Example:
|
| 87 |
+
|
| 88 |
+
# For discrete-time DPMs, given betas (the beta array for n = 0, 1, ..., N - 1):
|
| 89 |
+
>>> ns = NoiseScheduleVP('discrete', betas=betas)
|
| 90 |
+
|
| 91 |
+
# For discrete-time DPMs, given alphas_cumprod (the \hat{alpha_n} array for n = 0, 1, ..., N - 1):
|
| 92 |
+
>>> ns = NoiseScheduleVP('discrete', alphas_cumprod=alphas_cumprod)
|
| 93 |
+
|
| 94 |
+
# For continuous-time DPMs (VPSDE), linear schedule:
|
| 95 |
+
>>> ns = NoiseScheduleVP('linear', continuous_beta_0=0.1, continuous_beta_1=20.)
|
| 96 |
+
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
if schedule not in ['discrete', 'linear', 'cosine']:
|
| 100 |
+
raise ValueError("Unsupported noise schedule {}. The schedule needs to be 'discrete' or 'linear' or 'cosine'".format(schedule))
|
| 101 |
+
|
| 102 |
+
self.schedule = schedule
|
| 103 |
+
if schedule == 'discrete':
|
| 104 |
+
if betas is not None:
|
| 105 |
+
log_alphas = 0.5 * torch.log(1 - betas).cumsum(dim=0)
|
| 106 |
+
else:
|
| 107 |
+
assert alphas_cumprod is not None
|
| 108 |
+
log_alphas = 0.5 * torch.log(alphas_cumprod)
|
| 109 |
+
self.total_N = len(log_alphas)
|
| 110 |
+
self.T = 1.
|
| 111 |
+
self.t_array = torch.linspace(0., 1., self.total_N + 1)[1:].reshape((1, -1))
|
| 112 |
+
self.log_alpha_array = log_alphas.reshape((1, -1,))
|
| 113 |
+
else:
|
| 114 |
+
self.total_N = 1000
|
| 115 |
+
self.beta_0 = continuous_beta_0
|
| 116 |
+
self.beta_1 = continuous_beta_1
|
| 117 |
+
self.cosine_s = 0.008
|
| 118 |
+
self.cosine_beta_max = 999.
|
| 119 |
+
self.cosine_t_max = math.atan(self.cosine_beta_max * (1. + self.cosine_s) / math.pi) * 2. * (1. + self.cosine_s) / math.pi - self.cosine_s
|
| 120 |
+
self.cosine_log_alpha_0 = math.log(math.cos(self.cosine_s / (1. + self.cosine_s) * math.pi / 2.))
|
| 121 |
+
self.schedule = schedule
|
| 122 |
+
if schedule == 'cosine':
|
| 123 |
+
# For the cosine schedule, T = 1 will have numerical issues. So we manually set the ending time T.
|
| 124 |
+
# Note that T = 0.9946 may be not the optimal setting. However, we find it works well.
|
| 125 |
+
self.T = 0.9946
|
| 126 |
+
else:
|
| 127 |
+
self.T = 1.
|
| 128 |
+
|
| 129 |
+
def marginal_log_mean_coeff(self, t):
|
| 130 |
+
"""
|
| 131 |
+
Compute log(alpha_t) of a given continuous-time label t in [0, T].
|
| 132 |
+
"""
|
| 133 |
+
if self.schedule == 'discrete':
|
| 134 |
+
return interpolate_fn(t.reshape((-1, 1)), self.t_array.to(t.device), self.log_alpha_array.to(t.device)).reshape((-1))
|
| 135 |
+
elif self.schedule == 'linear':
|
| 136 |
+
return -0.25 * t ** 2 * (self.beta_1 - self.beta_0) - 0.5 * t * self.beta_0
|
| 137 |
+
elif self.schedule == 'cosine':
|
| 138 |
+
log_alpha_fn = lambda s: torch.log(torch.cos((s + self.cosine_s) / (1. + self.cosine_s) * math.pi / 2.))
|
| 139 |
+
log_alpha_t = log_alpha_fn(t) - self.cosine_log_alpha_0
|
| 140 |
+
return log_alpha_t
|
| 141 |
+
|
| 142 |
+
def marginal_alpha(self, t):
|
| 143 |
+
"""
|
| 144 |
+
Compute alpha_t of a given continuous-time label t in [0, T].
|
| 145 |
+
"""
|
| 146 |
+
return torch.exp(self.marginal_log_mean_coeff(t))
|
| 147 |
+
|
| 148 |
+
def marginal_std(self, t):
|
| 149 |
+
"""
|
| 150 |
+
Compute sigma_t of a given continuous-time label t in [0, T].
|
| 151 |
+
"""
|
| 152 |
+
return torch.sqrt(1. - torch.exp(2. * self.marginal_log_mean_coeff(t)))
|
| 153 |
+
|
| 154 |
+
def marginal_lambda(self, t):
|
| 155 |
+
"""
|
| 156 |
+
Compute lambda_t = log(alpha_t) - log(sigma_t) of a given continuous-time label t in [0, T].
|
| 157 |
+
"""
|
| 158 |
+
log_mean_coeff = self.marginal_log_mean_coeff(t)
|
| 159 |
+
log_std = 0.5 * torch.log(1. - torch.exp(2. * log_mean_coeff))
|
| 160 |
+
return log_mean_coeff - log_std
|
| 161 |
+
|
| 162 |
+
def inverse_lambda(self, lamb):
|
| 163 |
+
"""
|
| 164 |
+
Compute the continuous-time label t in [0, T] of a given half-logSNR lambda_t.
|
| 165 |
+
"""
|
| 166 |
+
if self.schedule == 'linear':
|
| 167 |
+
tmp = 2. * (self.beta_1 - self.beta_0) * torch.logaddexp(-2. * lamb, torch.zeros((1,)).to(lamb))
|
| 168 |
+
Delta = self.beta_0**2 + tmp
|
| 169 |
+
return tmp / (torch.sqrt(Delta) + self.beta_0) / (self.beta_1 - self.beta_0)
|
| 170 |
+
elif self.schedule == 'discrete':
|
| 171 |
+
log_alpha = -0.5 * torch.logaddexp(torch.zeros((1,)).to(lamb.device), -2. * lamb)
|
| 172 |
+
t = interpolate_fn(log_alpha.reshape((-1, 1)), torch.flip(self.log_alpha_array.to(lamb.device), [1]), torch.flip(self.t_array.to(lamb.device), [1]))
|
| 173 |
+
return t.reshape((-1,))
|
| 174 |
+
else:
|
| 175 |
+
log_alpha = -0.5 * torch.logaddexp(-2. * lamb, torch.zeros((1,)).to(lamb))
|
| 176 |
+
t_fn = lambda log_alpha_t: torch.arccos(torch.exp(log_alpha_t + self.cosine_log_alpha_0)) * 2. * (1. + self.cosine_s) / math.pi - self.cosine_s
|
| 177 |
+
t = t_fn(log_alpha)
|
| 178 |
+
return t
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def model_wrapper(
|
| 182 |
+
model,
|
| 183 |
+
noise_schedule,
|
| 184 |
+
model_type="noise",
|
| 185 |
+
model_kwargs={},
|
| 186 |
+
guidance_type="uncond",
|
| 187 |
+
condition=None,
|
| 188 |
+
unconditional_condition=None,
|
| 189 |
+
guidance_scale=1.,
|
| 190 |
+
classifier_fn=None,
|
| 191 |
+
classifier_kwargs={},
|
| 192 |
+
):
|
| 193 |
+
"""Create a wrapper function for the noise prediction model.
|
| 194 |
+
|
| 195 |
+
DPM-Solver needs to solve the continuous-time diffusion ODEs. For DPMs trained on discrete-time labels, we need to
|
| 196 |
+
firstly wrap the model function to a noise prediction model that accepts the continuous time as the input.
|
| 197 |
+
|
| 198 |
+
We support four types of the diffusion model by setting `model_type`:
|
| 199 |
+
|
| 200 |
+
1. "noise": noise prediction model. (Trained by predicting noise).
|
| 201 |
+
|
| 202 |
+
2. "x_start": data prediction model. (Trained by predicting the data x_0 at time 0).
|
| 203 |
+
|
| 204 |
+
3. "v": velocity prediction model. (Trained by predicting the velocity).
|
| 205 |
+
The "v" prediction is derivation detailed in Appendix D of [1], and is used in Imagen-Video [2].
|
| 206 |
+
|
| 207 |
+
[1] Salimans, Tim, and Jonathan Ho. "Progressive distillation for fast sampling of diffusion models."
|
| 208 |
+
arXiv preprint arXiv:2202.00512 (2022).
|
| 209 |
+
[2] Ho, Jonathan, et al. "Imagen Video: High Definition Video Generation with Diffusion Models."
|
| 210 |
+
arXiv preprint arXiv:2210.02303 (2022).
|
| 211 |
+
|
| 212 |
+
4. "score": marginal score function. (Trained by denoising score matching).
|
| 213 |
+
Note that the score function and the noise prediction model follows a simple relationship:
|
| 214 |
+
```
|
| 215 |
+
noise(x_t, t) = -sigma_t * score(x_t, t)
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
We support three types of guided sampling by DPMs by setting `guidance_type`:
|
| 219 |
+
1. "uncond": unconditional sampling by DPMs.
|
| 220 |
+
The input `model` has the following format:
|
| 221 |
+
``
|
| 222 |
+
model(x, t_input, **model_kwargs) -> noise | x_start | v | score
|
| 223 |
+
``
|
| 224 |
+
|
| 225 |
+
2. "classifier": classifier guidance sampling [3] by DPMs and another classifier.
|
| 226 |
+
The input `model` has the following format:
|
| 227 |
+
``
|
| 228 |
+
model(x, t_input, **model_kwargs) -> noise | x_start | v | score
|
| 229 |
+
``
|
| 230 |
+
|
| 231 |
+
The input `classifier_fn` has the following format:
|
| 232 |
+
``
|
| 233 |
+
classifier_fn(x, t_input, cond, **classifier_kwargs) -> logits(x, t_input, cond)
|
| 234 |
+
``
|
| 235 |
+
|
| 236 |
+
[3] P. Dhariwal and A. Q. Nichol, "Diffusion models beat GANs on image synthesis,"
|
| 237 |
+
in Advances in Neural Information Processing Systems, vol. 34, 2021, pp. 8780-8794.
|
| 238 |
+
|
| 239 |
+
3. "classifier-free": classifier-free guidance sampling by conditional DPMs.
|
| 240 |
+
The input `model` has the following format:
|
| 241 |
+
``
|
| 242 |
+
model(x, t_input, cond, **model_kwargs) -> noise | x_start | v | score
|
| 243 |
+
``
|
| 244 |
+
And if cond == `unconditional_condition`, the model output is the unconditional DPM output.
|
| 245 |
+
|
| 246 |
+
[4] Ho, Jonathan, and Tim Salimans. "Classifier-free diffusion guidance."
|
| 247 |
+
arXiv preprint arXiv:2207.12598 (2022).
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
The `t_input` is the time label of the model, which may be discrete-time labels (i.e. 0 to 999)
|
| 251 |
+
or continuous-time labels (i.e. epsilon to T).
|
| 252 |
+
|
| 253 |
+
We wrap the model function to accept only `x` and `t_continuous` as inputs, and outputs the predicted noise:
|
| 254 |
+
``
|
| 255 |
+
def model_fn(x, t_continuous) -> noise:
|
| 256 |
+
t_input = get_model_input_time(t_continuous)
|
| 257 |
+
return noise_pred(model, x, t_input, **model_kwargs)
|
| 258 |
+
``
|
| 259 |
+
where `t_continuous` is the continuous time labels (i.e. epsilon to T). And we use `model_fn` for DPM-Solver.
|
| 260 |
+
|
| 261 |
+
===============================================================
|
| 262 |
+
|
| 263 |
+
Args:
|
| 264 |
+
model: A diffusion model with the corresponding format described above.
|
| 265 |
+
noise_schedule: A noise schedule object, such as NoiseScheduleVP.
|
| 266 |
+
model_type: A `str`. The parameterization type of the diffusion model.
|
| 267 |
+
"noise" or "x_start" or "v" or "score".
|
| 268 |
+
model_kwargs: A `dict`. A dict for the other inputs of the model function.
|
| 269 |
+
guidance_type: A `str`. The type of the guidance for sampling.
|
| 270 |
+
"uncond" or "classifier" or "classifier-free".
|
| 271 |
+
condition: A pytorch tensor. The condition for the guided sampling.
|
| 272 |
+
Only used for "classifier" or "classifier-free" guidance type.
|
| 273 |
+
unconditional_condition: A pytorch tensor. The condition for the unconditional sampling.
|
| 274 |
+
Only used for "classifier-free" guidance type.
|
| 275 |
+
guidance_scale: A `float`. The scale for the guided sampling.
|
| 276 |
+
classifier_fn: A classifier function. Only used for the classifier guidance.
|
| 277 |
+
classifier_kwargs: A `dict`. A dict for the other inputs of the classifier function.
|
| 278 |
+
Returns:
|
| 279 |
+
A noise prediction model that accepts the noised data and the continuous time as the inputs.
|
| 280 |
+
"""
|
| 281 |
+
|
| 282 |
+
def get_model_input_time(t_continuous):
|
| 283 |
+
"""
|
| 284 |
+
Convert the continuous-time `t_continuous` (in [epsilon, T]) to the model input time.
|
| 285 |
+
For discrete-time DPMs, we convert `t_continuous` in [1 / N, 1] to `t_input` in [0, 1000 * (N - 1) / N].
|
| 286 |
+
For continuous-time DPMs, we just use `t_continuous`.
|
| 287 |
+
"""
|
| 288 |
+
if noise_schedule.schedule == 'discrete':
|
| 289 |
+
return (t_continuous - 1. / noise_schedule.total_N) * 1000.
|
| 290 |
+
else:
|
| 291 |
+
return t_continuous
|
| 292 |
+
|
| 293 |
+
def noise_pred_fn(x, t_continuous, cond=None):
|
| 294 |
+
if t_continuous.reshape((-1,)).shape[0] == 1:
|
| 295 |
+
t_continuous = t_continuous.expand((x.shape[0]))
|
| 296 |
+
t_input = get_model_input_time(t_continuous)
|
| 297 |
+
output = model(x, t_input, **model_kwargs)
|
| 298 |
+
if model_type == "noise":
|
| 299 |
+
return output
|
| 300 |
+
elif model_type == "x_start":
|
| 301 |
+
alpha_t, sigma_t = noise_schedule.marginal_alpha(t_continuous), noise_schedule.marginal_std(t_continuous)
|
| 302 |
+
dims = x.dim()
|
| 303 |
+
return (x - expand_dims(alpha_t, dims) * output) / expand_dims(sigma_t, dims)
|
| 304 |
+
elif model_type == "v":
|
| 305 |
+
alpha_t, sigma_t = noise_schedule.marginal_alpha(t_continuous), noise_schedule.marginal_std(t_continuous)
|
| 306 |
+
dims = x.dim()
|
| 307 |
+
return expand_dims(alpha_t, dims) * output + expand_dims(sigma_t, dims) * x
|
| 308 |
+
elif model_type == "score":
|
| 309 |
+
sigma_t = noise_schedule.marginal_std(t_continuous)
|
| 310 |
+
dims = x.dim()
|
| 311 |
+
return -expand_dims(sigma_t, dims) * output
|
| 312 |
+
|
| 313 |
+
def cond_grad_fn(x, t_input):
|
| 314 |
+
"""
|
| 315 |
+
Compute the gradient of the classifier, i.e. nabla_{x} log p_t(cond | x_t).
|
| 316 |
+
"""
|
| 317 |
+
with torch.enable_grad():
|
| 318 |
+
x_in = x.detach().requires_grad_(True)
|
| 319 |
+
log_prob = classifier_fn(x_in, t_input, condition, **classifier_kwargs)
|
| 320 |
+
return torch.autograd.grad(log_prob.sum(), x_in)[0]
|
| 321 |
+
|
| 322 |
+
def model_fn(x, t_continuous):
|
| 323 |
+
"""
|
| 324 |
+
The noise predicition model function that is used for DPM-Solver.
|
| 325 |
+
"""
|
| 326 |
+
if t_continuous.reshape((-1,)).shape[0] == 1:
|
| 327 |
+
t_continuous = t_continuous.expand((x.shape[0]))
|
| 328 |
+
if guidance_type == "uncond":
|
| 329 |
+
return noise_pred_fn(x, t_continuous)
|
| 330 |
+
elif guidance_type == "classifier":
|
| 331 |
+
assert classifier_fn is not None
|
| 332 |
+
t_input = get_model_input_time(t_continuous)
|
| 333 |
+
cond_grad = cond_grad_fn(x, t_input)
|
| 334 |
+
sigma_t = noise_schedule.marginal_std(t_continuous)
|
| 335 |
+
noise = noise_pred_fn(x, t_continuous)
|
| 336 |
+
return noise - guidance_scale * expand_dims(sigma_t, dims=cond_grad.dim()) * cond_grad
|
| 337 |
+
elif guidance_type == "classifier-free":
|
| 338 |
+
if guidance_scale == 1. or unconditional_condition is None:
|
| 339 |
+
return noise_pred_fn(x, t_continuous, cond=condition)
|
| 340 |
+
else:
|
| 341 |
+
x_in = torch.cat([x] * 2)
|
| 342 |
+
t_in = torch.cat([t_continuous] * 2)
|
| 343 |
+
c_in = torch.cat([unconditional_condition, condition])
|
| 344 |
+
noise_uncond, noise = noise_pred_fn(x_in, t_in, cond=c_in).chunk(2)
|
| 345 |
+
return noise_uncond + guidance_scale * (noise - noise_uncond)
|
| 346 |
+
|
| 347 |
+
assert model_type in ["noise", "x_start", "v"]
|
| 348 |
+
assert guidance_type in ["uncond", "classifier", "classifier-free"]
|
| 349 |
+
return model_fn
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
class UniPC:
|
| 353 |
+
def __init__(
|
| 354 |
+
self,
|
| 355 |
+
model_fn,
|
| 356 |
+
noise_schedule,
|
| 357 |
+
predict_x0=True,
|
| 358 |
+
thresholding=False,
|
| 359 |
+
max_val=1.,
|
| 360 |
+
variant='bh1',
|
| 361 |
+
):
|
| 362 |
+
"""Construct a UniPC.
|
| 363 |
+
|
| 364 |
+
We support both data_prediction and noise_prediction.
|
| 365 |
+
"""
|
| 366 |
+
self.model = model_fn
|
| 367 |
+
self.noise_schedule = noise_schedule
|
| 368 |
+
self.variant = variant
|
| 369 |
+
self.predict_x0 = predict_x0
|
| 370 |
+
self.thresholding = thresholding
|
| 371 |
+
self.max_val = max_val
|
| 372 |
+
|
| 373 |
+
def dynamic_thresholding_fn(self, x0, t=None):
|
| 374 |
+
"""
|
| 375 |
+
The dynamic thresholding method.
|
| 376 |
+
"""
|
| 377 |
+
dims = x0.dim()
|
| 378 |
+
p = self.dynamic_thresholding_ratio
|
| 379 |
+
s = torch.quantile(torch.abs(x0).reshape((x0.shape[0], -1)), p, dim=1)
|
| 380 |
+
s = expand_dims(torch.maximum(s, self.thresholding_max_val * torch.ones_like(s).to(s.device)), dims)
|
| 381 |
+
x0 = torch.clamp(x0, -s, s) / s
|
| 382 |
+
return x0
|
| 383 |
+
|
| 384 |
+
def noise_prediction_fn(self, x, t):
|
| 385 |
+
"""
|
| 386 |
+
Return the noise prediction model.
|
| 387 |
+
"""
|
| 388 |
+
return self.model(x, t)
|
| 389 |
+
|
| 390 |
+
def data_prediction_fn(self, x, t):
|
| 391 |
+
"""
|
| 392 |
+
Return the data prediction model (with thresholding).
|
| 393 |
+
"""
|
| 394 |
+
noise = self.noise_prediction_fn(x, t)
|
| 395 |
+
dims = x.dim()
|
| 396 |
+
alpha_t, sigma_t = self.noise_schedule.marginal_alpha(t), self.noise_schedule.marginal_std(t)
|
| 397 |
+
x0 = (x - expand_dims(sigma_t, dims) * noise) / expand_dims(alpha_t, dims)
|
| 398 |
+
if self.thresholding:
|
| 399 |
+
p = 0.995 # A hyperparameter in the paper of "Imagen" [1].
|
| 400 |
+
s = torch.quantile(torch.abs(x0).reshape((x0.shape[0], -1)), p, dim=1)
|
| 401 |
+
s = expand_dims(torch.maximum(s, self.max_val * torch.ones_like(s).to(s.device)), dims)
|
| 402 |
+
x0 = torch.clamp(x0, -s, s) / s
|
| 403 |
+
return x0
|
| 404 |
+
|
| 405 |
+
def model_fn(self, x, t):
|
| 406 |
+
"""
|
| 407 |
+
Convert the model to the noise prediction model or the data prediction model.
|
| 408 |
+
"""
|
| 409 |
+
if self.predict_x0:
|
| 410 |
+
return self.data_prediction_fn(x, t)
|
| 411 |
+
else:
|
| 412 |
+
return self.noise_prediction_fn(x, t)
|
| 413 |
+
|
| 414 |
+
def get_time_steps(self, skip_type, t_T, t_0, N, device):
|
| 415 |
+
"""Compute the intermediate time steps for sampling.
|
| 416 |
+
"""
|
| 417 |
+
if skip_type == 'logSNR':
|
| 418 |
+
lambda_T = self.noise_schedule.marginal_lambda(torch.tensor(t_T).to(device))
|
| 419 |
+
lambda_0 = self.noise_schedule.marginal_lambda(torch.tensor(t_0).to(device))
|
| 420 |
+
logSNR_steps = torch.linspace(lambda_T.cpu().item(), lambda_0.cpu().item(), N + 1).to(device)
|
| 421 |
+
return self.noise_schedule.inverse_lambda(logSNR_steps)
|
| 422 |
+
elif skip_type == 'time_uniform':
|
| 423 |
+
return torch.linspace(t_T, t_0, N + 1).to(device)
|
| 424 |
+
elif skip_type == 'time_quadratic':
|
| 425 |
+
t_order = 2
|
| 426 |
+
t = torch.linspace(t_T**(1. / t_order), t_0**(1. / t_order), N + 1).pow(t_order).to(device)
|
| 427 |
+
return t
|
| 428 |
+
else:
|
| 429 |
+
raise ValueError("Unsupported skip_type {}, need to be 'logSNR' or 'time_uniform' or 'time_quadratic'".format(skip_type))
|
| 430 |
+
|
| 431 |
+
def get_orders_and_timesteps_for_singlestep_solver(self, steps, order, skip_type, t_T, t_0, device):
|
| 432 |
+
"""
|
| 433 |
+
Get the order of each step for sampling by the singlestep DPM-Solver.
|
| 434 |
+
"""
|
| 435 |
+
if order == 3:
|
| 436 |
+
K = steps // 3 + 1
|
| 437 |
+
if steps % 3 == 0:
|
| 438 |
+
orders = [3,] * (K - 2) + [2, 1]
|
| 439 |
+
elif steps % 3 == 1:
|
| 440 |
+
orders = [3,] * (K - 1) + [1]
|
| 441 |
+
else:
|
| 442 |
+
orders = [3,] * (K - 1) + [2]
|
| 443 |
+
elif order == 2:
|
| 444 |
+
if steps % 2 == 0:
|
| 445 |
+
K = steps // 2
|
| 446 |
+
orders = [2,] * K
|
| 447 |
+
else:
|
| 448 |
+
K = steps // 2 + 1
|
| 449 |
+
orders = [2,] * (K - 1) + [1]
|
| 450 |
+
elif order == 1:
|
| 451 |
+
K = steps
|
| 452 |
+
orders = [1,] * steps
|
| 453 |
+
else:
|
| 454 |
+
raise ValueError("'order' must be '1' or '2' or '3'.")
|
| 455 |
+
if skip_type == 'logSNR':
|
| 456 |
+
# To reproduce the results in DPM-Solver paper
|
| 457 |
+
timesteps_outer = self.get_time_steps(skip_type, t_T, t_0, K, device)
|
| 458 |
+
else:
|
| 459 |
+
timesteps_outer = self.get_time_steps(skip_type, t_T, t_0, steps, device)[torch.cumsum(torch.tensor([0,] + orders), 0).to(device)]
|
| 460 |
+
return timesteps_outer, orders
|
| 461 |
+
|
| 462 |
+
def denoise_to_zero_fn(self, x, s):
|
| 463 |
+
"""
|
| 464 |
+
Denoise at the final step, which is equivalent to solve the ODE from lambda_s to infty by first-order discretization.
|
| 465 |
+
"""
|
| 466 |
+
return self.data_prediction_fn(x, s)
|
| 467 |
+
|
| 468 |
+
def multistep_uni_pc_update(self, x, model_prev_list, t_prev_list, t, order, **kwargs):
|
| 469 |
+
if len(t.shape) == 0:
|
| 470 |
+
t = t.view(-1)
|
| 471 |
+
if 'bh' in self.variant:
|
| 472 |
+
return self.multistep_uni_pc_bh_update(x, model_prev_list, t_prev_list, t, order, **kwargs)
|
| 473 |
+
else:
|
| 474 |
+
assert self.variant == 'vary_coeff'
|
| 475 |
+
return self.multistep_uni_pc_vary_update(x, model_prev_list, t_prev_list, t, order, **kwargs)
|
| 476 |
+
|
| 477 |
+
def multistep_uni_pc_vary_update(self, x, model_prev_list, t_prev_list, t, order, use_corrector=True):
|
| 478 |
+
logging.info(f'using unified predictor-corrector with order {order} (solver type: vary coeff)')
|
| 479 |
+
ns = self.noise_schedule
|
| 480 |
+
assert order <= len(model_prev_list)
|
| 481 |
+
|
| 482 |
+
# first compute rks
|
| 483 |
+
t_prev_0 = t_prev_list[-1]
|
| 484 |
+
lambda_prev_0 = ns.marginal_lambda(t_prev_0)
|
| 485 |
+
lambda_t = ns.marginal_lambda(t)
|
| 486 |
+
model_prev_0 = model_prev_list[-1]
|
| 487 |
+
sigma_prev_0, sigma_t = ns.marginal_std(t_prev_0), ns.marginal_std(t)
|
| 488 |
+
log_alpha_t = ns.marginal_log_mean_coeff(t)
|
| 489 |
+
alpha_t = torch.exp(log_alpha_t)
|
| 490 |
+
|
| 491 |
+
h = lambda_t - lambda_prev_0
|
| 492 |
+
|
| 493 |
+
rks = []
|
| 494 |
+
D1s = []
|
| 495 |
+
for i in range(1, order):
|
| 496 |
+
t_prev_i = t_prev_list[-(i + 1)]
|
| 497 |
+
model_prev_i = model_prev_list[-(i + 1)]
|
| 498 |
+
lambda_prev_i = ns.marginal_lambda(t_prev_i)
|
| 499 |
+
rk = (lambda_prev_i - lambda_prev_0) / h
|
| 500 |
+
rks.append(rk)
|
| 501 |
+
D1s.append((model_prev_i - model_prev_0) / rk)
|
| 502 |
+
|
| 503 |
+
rks.append(1.)
|
| 504 |
+
rks = torch.tensor(rks, device=x.device)
|
| 505 |
+
|
| 506 |
+
K = len(rks)
|
| 507 |
+
# build C matrix
|
| 508 |
+
C = []
|
| 509 |
+
|
| 510 |
+
col = torch.ones_like(rks)
|
| 511 |
+
for k in range(1, K + 1):
|
| 512 |
+
C.append(col)
|
| 513 |
+
col = col * rks / (k + 1)
|
| 514 |
+
C = torch.stack(C, dim=1)
|
| 515 |
+
|
| 516 |
+
if len(D1s) > 0:
|
| 517 |
+
D1s = torch.stack(D1s, dim=1) # (B, K)
|
| 518 |
+
C_inv_p = torch.linalg.inv(C[:-1, :-1])
|
| 519 |
+
A_p = C_inv_p
|
| 520 |
+
|
| 521 |
+
if use_corrector:
|
| 522 |
+
C_inv = torch.linalg.inv(C)
|
| 523 |
+
A_c = C_inv
|
| 524 |
+
|
| 525 |
+
hh = -h if self.predict_x0 else h
|
| 526 |
+
h_phi_1 = torch.expm1(hh)
|
| 527 |
+
h_phi_ks = []
|
| 528 |
+
factorial_k = 1
|
| 529 |
+
h_phi_k = h_phi_1
|
| 530 |
+
for k in range(1, K + 2):
|
| 531 |
+
h_phi_ks.append(h_phi_k)
|
| 532 |
+
h_phi_k = h_phi_k / hh - 1 / factorial_k
|
| 533 |
+
factorial_k *= (k + 1)
|
| 534 |
+
|
| 535 |
+
model_t = None
|
| 536 |
+
if self.predict_x0:
|
| 537 |
+
x_t_ = (
|
| 538 |
+
sigma_t / sigma_prev_0 * x
|
| 539 |
+
- alpha_t * h_phi_1 * model_prev_0
|
| 540 |
+
)
|
| 541 |
+
# now predictor
|
| 542 |
+
x_t = x_t_
|
| 543 |
+
if len(D1s) > 0:
|
| 544 |
+
# compute the residuals for predictor
|
| 545 |
+
for k in range(K - 1):
|
| 546 |
+
x_t = x_t - alpha_t * h_phi_ks[k + 1] * torch.einsum('bkchw,k->bchw', D1s, A_p[k])
|
| 547 |
+
# now corrector
|
| 548 |
+
if use_corrector:
|
| 549 |
+
model_t = self.model_fn(x_t, t)
|
| 550 |
+
D1_t = (model_t - model_prev_0)
|
| 551 |
+
x_t = x_t_
|
| 552 |
+
k = 0
|
| 553 |
+
for k in range(K - 1):
|
| 554 |
+
x_t = x_t - alpha_t * h_phi_ks[k + 1] * torch.einsum('bkchw,k->bchw', D1s, A_c[k][:-1])
|
| 555 |
+
x_t = x_t - alpha_t * h_phi_ks[K] * (D1_t * A_c[k][-1])
|
| 556 |
+
else:
|
| 557 |
+
log_alpha_prev_0, log_alpha_t = ns.marginal_log_mean_coeff(t_prev_0), ns.marginal_log_mean_coeff(t)
|
| 558 |
+
x_t_ = (
|
| 559 |
+
(torch.exp(log_alpha_t - log_alpha_prev_0)) * x
|
| 560 |
+
- (sigma_t * h_phi_1) * model_prev_0
|
| 561 |
+
)
|
| 562 |
+
# now predictor
|
| 563 |
+
x_t = x_t_
|
| 564 |
+
if len(D1s) > 0:
|
| 565 |
+
# compute the residuals for predictor
|
| 566 |
+
for k in range(K - 1):
|
| 567 |
+
x_t = x_t - sigma_t * h_phi_ks[k + 1] * torch.einsum('bkchw,k->bchw', D1s, A_p[k])
|
| 568 |
+
# now corrector
|
| 569 |
+
if use_corrector:
|
| 570 |
+
model_t = self.model_fn(x_t, t)
|
| 571 |
+
D1_t = (model_t - model_prev_0)
|
| 572 |
+
x_t = x_t_
|
| 573 |
+
k = 0
|
| 574 |
+
for k in range(K - 1):
|
| 575 |
+
x_t = x_t - sigma_t * h_phi_ks[k + 1] * torch.einsum('bkchw,k->bchw', D1s, A_c[k][:-1])
|
| 576 |
+
x_t = x_t - sigma_t * h_phi_ks[K] * (D1_t * A_c[k][-1])
|
| 577 |
+
return x_t, model_t
|
| 578 |
+
|
| 579 |
+
def multistep_uni_pc_bh_update(self, x, model_prev_list, t_prev_list, t, order, x_t=None, use_corrector=True):
|
| 580 |
+
# print(f'using unified predictor-corrector with order {order} (solver type: B(h))')
|
| 581 |
+
ns = self.noise_schedule
|
| 582 |
+
assert order <= len(model_prev_list)
|
| 583 |
+
dims = x.dim()
|
| 584 |
+
|
| 585 |
+
# first compute rks
|
| 586 |
+
t_prev_0 = t_prev_list[-1]
|
| 587 |
+
lambda_prev_0 = ns.marginal_lambda(t_prev_0)
|
| 588 |
+
lambda_t = ns.marginal_lambda(t)
|
| 589 |
+
model_prev_0 = model_prev_list[-1]
|
| 590 |
+
sigma_prev_0, sigma_t = ns.marginal_std(t_prev_0), ns.marginal_std(t)
|
| 591 |
+
log_alpha_prev_0, log_alpha_t = ns.marginal_log_mean_coeff(t_prev_0), ns.marginal_log_mean_coeff(t)
|
| 592 |
+
alpha_t = torch.exp(log_alpha_t)
|
| 593 |
+
|
| 594 |
+
h = lambda_t - lambda_prev_0
|
| 595 |
+
|
| 596 |
+
rks = []
|
| 597 |
+
D1s = []
|
| 598 |
+
for i in range(1, order):
|
| 599 |
+
t_prev_i = t_prev_list[-(i + 1)]
|
| 600 |
+
model_prev_i = model_prev_list[-(i + 1)]
|
| 601 |
+
lambda_prev_i = ns.marginal_lambda(t_prev_i)
|
| 602 |
+
rk = ((lambda_prev_i - lambda_prev_0) / h)[0]
|
| 603 |
+
rks.append(rk)
|
| 604 |
+
D1s.append((model_prev_i - model_prev_0) / rk)
|
| 605 |
+
|
| 606 |
+
rks.append(1.)
|
| 607 |
+
rks = torch.tensor(rks, device=x.device)
|
| 608 |
+
|
| 609 |
+
R = []
|
| 610 |
+
b = []
|
| 611 |
+
|
| 612 |
+
hh = -h[0] if self.predict_x0 else h[0]
|
| 613 |
+
h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1
|
| 614 |
+
h_phi_k = h_phi_1 / hh - 1
|
| 615 |
+
|
| 616 |
+
factorial_i = 1
|
| 617 |
+
|
| 618 |
+
if self.variant == 'bh1':
|
| 619 |
+
B_h = hh
|
| 620 |
+
elif self.variant == 'bh2':
|
| 621 |
+
B_h = torch.expm1(hh)
|
| 622 |
+
else:
|
| 623 |
+
raise NotImplementedError()
|
| 624 |
+
|
| 625 |
+
for i in range(1, order + 1):
|
| 626 |
+
R.append(torch.pow(rks, i - 1))
|
| 627 |
+
b.append(h_phi_k * factorial_i / B_h)
|
| 628 |
+
factorial_i *= (i + 1)
|
| 629 |
+
h_phi_k = h_phi_k / hh - 1 / factorial_i
|
| 630 |
+
|
| 631 |
+
R = torch.stack(R)
|
| 632 |
+
b = torch.tensor(b, device=x.device)
|
| 633 |
+
|
| 634 |
+
# now predictor
|
| 635 |
+
use_predictor = len(D1s) > 0 and x_t is None
|
| 636 |
+
if len(D1s) > 0:
|
| 637 |
+
D1s = torch.stack(D1s, dim=1) # (B, K)
|
| 638 |
+
if x_t is None:
|
| 639 |
+
# for order 2, we use a simplified version
|
| 640 |
+
if order == 2:
|
| 641 |
+
rhos_p = torch.tensor([0.5], device=b.device)
|
| 642 |
+
else:
|
| 643 |
+
rhos_p = torch.linalg.solve(R[:-1, :-1], b[:-1])
|
| 644 |
+
else:
|
| 645 |
+
D1s = None
|
| 646 |
+
|
| 647 |
+
if use_corrector:
|
| 648 |
+
# print('using corrector')
|
| 649 |
+
# for order 1, we use a simplified version
|
| 650 |
+
if order == 1:
|
| 651 |
+
rhos_c = torch.tensor([0.5], device=b.device)
|
| 652 |
+
else:
|
| 653 |
+
rhos_c = torch.linalg.solve(R, b)
|
| 654 |
+
|
| 655 |
+
model_t = None
|
| 656 |
+
if self.predict_x0:
|
| 657 |
+
x_t_ = (
|
| 658 |
+
expand_dims(sigma_t / sigma_prev_0, dims) * x
|
| 659 |
+
- expand_dims(alpha_t * h_phi_1, dims)* model_prev_0
|
| 660 |
+
)
|
| 661 |
+
|
| 662 |
+
if x_t is None:
|
| 663 |
+
if use_predictor:
|
| 664 |
+
pred_res = torch.tensordot(D1s, rhos_p, dims=([1], [0])) # torch.einsum('k,bkchw->bchw', rhos_p, D1s)
|
| 665 |
+
else:
|
| 666 |
+
pred_res = 0
|
| 667 |
+
x_t = x_t_ - expand_dims(alpha_t * B_h, dims) * pred_res
|
| 668 |
+
|
| 669 |
+
if use_corrector:
|
| 670 |
+
model_t = self.model_fn(x_t, t)
|
| 671 |
+
if D1s is not None:
|
| 672 |
+
corr_res = torch.tensordot(D1s, rhos_c[:-1], dims=([1], [0])) # torch.einsum('k,bkchw->bchw', rhos_c[:-1], D1s)
|
| 673 |
+
else:
|
| 674 |
+
corr_res = 0
|
| 675 |
+
D1_t = (model_t - model_prev_0)
|
| 676 |
+
x_t = x_t_ - expand_dims(alpha_t * B_h, dims) * (corr_res + rhos_c[-1] * D1_t)
|
| 677 |
+
else:
|
| 678 |
+
x_t_ = (
|
| 679 |
+
expand_dims(torch.exp(log_alpha_t - log_alpha_prev_0), dims) * x
|
| 680 |
+
- expand_dims(sigma_t * h_phi_1, dims) * model_prev_0
|
| 681 |
+
)
|
| 682 |
+
if x_t is None:
|
| 683 |
+
if use_predictor:
|
| 684 |
+
pred_res = torch.einsum('k,bkchw->bchw', rhos_p, D1s)
|
| 685 |
+
else:
|
| 686 |
+
pred_res = 0
|
| 687 |
+
x_t = x_t_ - expand_dims(sigma_t * B_h, dims) * pred_res
|
| 688 |
+
|
| 689 |
+
if use_corrector:
|
| 690 |
+
model_t = self.model_fn(x_t, t)
|
| 691 |
+
if D1s is not None:
|
| 692 |
+
corr_res = torch.einsum('k,bkchw->bchw', rhos_c[:-1], D1s)
|
| 693 |
+
else:
|
| 694 |
+
corr_res = 0
|
| 695 |
+
D1_t = (model_t - model_prev_0)
|
| 696 |
+
x_t = x_t_ - expand_dims(sigma_t * B_h, dims) * (corr_res + rhos_c[-1] * D1_t)
|
| 697 |
+
return x_t, model_t
|
| 698 |
+
|
| 699 |
+
|
| 700 |
+
def sample(self, x, timesteps, t_start=None, t_end=None, order=3, skip_type='time_uniform',
|
| 701 |
+
method='singlestep', lower_order_final=True, denoise_to_zero=False, solver_type='dpm_solver',
|
| 702 |
+
atol=0.0078, rtol=0.05, corrector=False, callback=None, disable_pbar=False
|
| 703 |
+
):
|
| 704 |
+
# t_0 = 1. / self.noise_schedule.total_N if t_end is None else t_end
|
| 705 |
+
# t_T = self.noise_schedule.T if t_start is None else t_start
|
| 706 |
+
steps = len(timesteps) - 1
|
| 707 |
+
if method == 'multistep':
|
| 708 |
+
assert steps >= order
|
| 709 |
+
# timesteps = self.get_time_steps(skip_type=skip_type, t_T=t_T, t_0=t_0, N=steps, device=device)
|
| 710 |
+
assert timesteps.shape[0] - 1 == steps
|
| 711 |
+
# with torch.no_grad():
|
| 712 |
+
for step_index in trange(steps, disable=disable_pbar):
|
| 713 |
+
if step_index == 0:
|
| 714 |
+
vec_t = timesteps[0].expand((x.shape[0]))
|
| 715 |
+
model_prev_list = [self.model_fn(x, vec_t)]
|
| 716 |
+
t_prev_list = [vec_t]
|
| 717 |
+
elif step_index < order:
|
| 718 |
+
init_order = step_index
|
| 719 |
+
# Init the first `order` values by lower order multistep DPM-Solver.
|
| 720 |
+
# for init_order in range(1, order):
|
| 721 |
+
vec_t = timesteps[init_order].expand(x.shape[0])
|
| 722 |
+
x, model_x = self.multistep_uni_pc_update(x, model_prev_list, t_prev_list, vec_t, init_order, use_corrector=True)
|
| 723 |
+
if model_x is None:
|
| 724 |
+
model_x = self.model_fn(x, vec_t)
|
| 725 |
+
model_prev_list.append(model_x)
|
| 726 |
+
t_prev_list.append(vec_t)
|
| 727 |
+
else:
|
| 728 |
+
extra_final_step = 0
|
| 729 |
+
if step_index == (steps - 1):
|
| 730 |
+
extra_final_step = 1
|
| 731 |
+
for step in range(step_index, step_index + 1 + extra_final_step):
|
| 732 |
+
vec_t = timesteps[step].expand(x.shape[0])
|
| 733 |
+
if lower_order_final:
|
| 734 |
+
step_order = min(order, steps + 1 - step)
|
| 735 |
+
else:
|
| 736 |
+
step_order = order
|
| 737 |
+
# print('this step order:', step_order)
|
| 738 |
+
if step == steps:
|
| 739 |
+
# print('do not run corrector at the last step')
|
| 740 |
+
use_corrector = False
|
| 741 |
+
else:
|
| 742 |
+
use_corrector = True
|
| 743 |
+
x, model_x = self.multistep_uni_pc_update(x, model_prev_list, t_prev_list, vec_t, step_order, use_corrector=use_corrector)
|
| 744 |
+
for i in range(order - 1):
|
| 745 |
+
t_prev_list[i] = t_prev_list[i + 1]
|
| 746 |
+
model_prev_list[i] = model_prev_list[i + 1]
|
| 747 |
+
t_prev_list[-1] = vec_t
|
| 748 |
+
# We do not need to evaluate the final model value.
|
| 749 |
+
if step < steps:
|
| 750 |
+
if model_x is None:
|
| 751 |
+
model_x = self.model_fn(x, vec_t)
|
| 752 |
+
model_prev_list[-1] = model_x
|
| 753 |
+
if callback is not None:
|
| 754 |
+
callback({'x': x, 'i': step_index, 'denoised': model_prev_list[-1]})
|
| 755 |
+
else:
|
| 756 |
+
raise NotImplementedError()
|
| 757 |
+
# if denoise_to_zero:
|
| 758 |
+
# x = self.denoise_to_zero_fn(x, torch.ones((x.shape[0],)).to(device) * t_0)
|
| 759 |
+
return x
|
| 760 |
+
|
| 761 |
+
|
| 762 |
+
#############################################################
|
| 763 |
+
# other utility functions
|
| 764 |
+
#############################################################
|
| 765 |
+
|
| 766 |
+
def interpolate_fn(x, xp, yp):
|
| 767 |
+
"""
|
| 768 |
+
A piecewise linear function y = f(x), using xp and yp as keypoints.
|
| 769 |
+
We implement f(x) in a differentiable way (i.e. applicable for autograd).
|
| 770 |
+
The function f(x) is well-defined for all x-axis. (For x beyond the bounds of xp, we use the outmost points of xp to define the linear function.)
|
| 771 |
+
|
| 772 |
+
Args:
|
| 773 |
+
x: PyTorch tensor with shape [N, C], where N is the batch size, C is the number of channels (we use C = 1 for DPM-Solver).
|
| 774 |
+
xp: PyTorch tensor with shape [C, K], where K is the number of keypoints.
|
| 775 |
+
yp: PyTorch tensor with shape [C, K].
|
| 776 |
+
Returns:
|
| 777 |
+
The function values f(x), with shape [N, C].
|
| 778 |
+
"""
|
| 779 |
+
N, K = x.shape[0], xp.shape[1]
|
| 780 |
+
all_x = torch.cat([x.unsqueeze(2), xp.unsqueeze(0).repeat((N, 1, 1))], dim=2)
|
| 781 |
+
sorted_all_x, x_indices = torch.sort(all_x, dim=2)
|
| 782 |
+
x_idx = torch.argmin(x_indices, dim=2)
|
| 783 |
+
cand_start_idx = x_idx - 1
|
| 784 |
+
start_idx = torch.where(
|
| 785 |
+
torch.eq(x_idx, 0),
|
| 786 |
+
torch.tensor(1, device=x.device),
|
| 787 |
+
torch.where(
|
| 788 |
+
torch.eq(x_idx, K), torch.tensor(K - 2, device=x.device), cand_start_idx,
|
| 789 |
+
),
|
| 790 |
+
)
|
| 791 |
+
end_idx = torch.where(torch.eq(start_idx, cand_start_idx), start_idx + 2, start_idx + 1)
|
| 792 |
+
start_x = torch.gather(sorted_all_x, dim=2, index=start_idx.unsqueeze(2)).squeeze(2)
|
| 793 |
+
end_x = torch.gather(sorted_all_x, dim=2, index=end_idx.unsqueeze(2)).squeeze(2)
|
| 794 |
+
start_idx2 = torch.where(
|
| 795 |
+
torch.eq(x_idx, 0),
|
| 796 |
+
torch.tensor(0, device=x.device),
|
| 797 |
+
torch.where(
|
| 798 |
+
torch.eq(x_idx, K), torch.tensor(K - 2, device=x.device), cand_start_idx,
|
| 799 |
+
),
|
| 800 |
+
)
|
| 801 |
+
y_positions_expanded = yp.unsqueeze(0).expand(N, -1, -1)
|
| 802 |
+
start_y = torch.gather(y_positions_expanded, dim=2, index=start_idx2.unsqueeze(2)).squeeze(2)
|
| 803 |
+
end_y = torch.gather(y_positions_expanded, dim=2, index=(start_idx2 + 1).unsqueeze(2)).squeeze(2)
|
| 804 |
+
cand = start_y + (x - start_x) * (end_y - start_y) / (end_x - start_x)
|
| 805 |
+
return cand
|
| 806 |
+
|
| 807 |
+
|
| 808 |
+
def expand_dims(v, dims):
|
| 809 |
+
"""
|
| 810 |
+
Expand the tensor `v` to the dim `dims`.
|
| 811 |
+
|
| 812 |
+
Args:
|
| 813 |
+
`v`: a PyTorch tensor with shape [N].
|
| 814 |
+
`dim`: a `int`.
|
| 815 |
+
Returns:
|
| 816 |
+
a PyTorch tensor with shape [N, 1, 1, ..., 1] and the total dimension is `dims`.
|
| 817 |
+
"""
|
| 818 |
+
return v[(...,) + (None,)*(dims - 1)]
|
| 819 |
+
|
| 820 |
+
|
| 821 |
+
class SigmaConvert:
|
| 822 |
+
schedule = ""
|
| 823 |
+
def marginal_log_mean_coeff(self, sigma):
|
| 824 |
+
return 0.5 * torch.log(1 / ((sigma * sigma) + 1))
|
| 825 |
+
|
| 826 |
+
def marginal_alpha(self, t):
|
| 827 |
+
return torch.exp(self.marginal_log_mean_coeff(t))
|
| 828 |
+
|
| 829 |
+
def marginal_std(self, t):
|
| 830 |
+
return torch.sqrt(1. - torch.exp(2. * self.marginal_log_mean_coeff(t)))
|
| 831 |
+
|
| 832 |
+
def marginal_lambda(self, t):
|
| 833 |
+
"""
|
| 834 |
+
Compute lambda_t = log(alpha_t) - log(sigma_t) of a given continuous-time label t in [0, T].
|
| 835 |
+
"""
|
| 836 |
+
log_mean_coeff = self.marginal_log_mean_coeff(t)
|
| 837 |
+
log_std = 0.5 * torch.log(1. - torch.exp(2. * log_mean_coeff))
|
| 838 |
+
return log_mean_coeff - log_std
|
| 839 |
+
|
| 840 |
+
def predict_eps_sigma(model, input, sigma_in, **kwargs):
|
| 841 |
+
sigma = sigma_in.view(sigma_in.shape[:1] + (1,) * (input.ndim - 1))
|
| 842 |
+
input = input * ((sigma ** 2 + 1.0) ** 0.5)
|
| 843 |
+
return (input - model(input, sigma_in, **kwargs)) / sigma
|
| 844 |
+
|
| 845 |
+
|
| 846 |
+
def sample_unipc(model, noise, sigmas, extra_args=None, callback=None, disable=False, variant='bh1'):
|
| 847 |
+
timesteps = sigmas.clone()
|
| 848 |
+
if sigmas[-1] == 0:
|
| 849 |
+
timesteps = sigmas[:]
|
| 850 |
+
timesteps[-1] = 0.001
|
| 851 |
+
else:
|
| 852 |
+
timesteps = sigmas.clone()
|
| 853 |
+
ns = SigmaConvert()
|
| 854 |
+
|
| 855 |
+
noise = noise / torch.sqrt(1.0 + timesteps[0] ** 2.0)
|
| 856 |
+
model_type = "noise"
|
| 857 |
+
|
| 858 |
+
model_fn = model_wrapper(
|
| 859 |
+
lambda input, sigma, **kwargs: predict_eps_sigma(model, input, sigma, **kwargs),
|
| 860 |
+
ns,
|
| 861 |
+
model_type=model_type,
|
| 862 |
+
guidance_type="uncond",
|
| 863 |
+
model_kwargs=extra_args,
|
| 864 |
+
)
|
| 865 |
+
|
| 866 |
+
order = min(3, len(timesteps) - 2)
|
| 867 |
+
uni_pc = UniPC(model_fn, ns, predict_x0=True, thresholding=False, variant=variant)
|
| 868 |
+
x = uni_pc.sample(noise, timesteps=timesteps, skip_type="time_uniform", method="multistep", order=order, lower_order_final=True, callback=callback, disable_pbar=disable)
|
| 869 |
+
x /= ns.marginal_alpha(timesteps[-1])
|
| 870 |
+
return x
|
| 871 |
+
|
| 872 |
+
def sample_unipc_bh2(model, noise, sigmas, extra_args=None, callback=None, disable=False):
|
| 873 |
+
return sample_unipc(model, noise, sigmas, extra_args, callback, disable, variant='bh2')
|
comfy/float.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
_CK_STOCHASTIC_ROUNDING_AVAILABLE = False
|
| 6 |
+
try:
|
| 7 |
+
import comfy_kitchen as ck
|
| 8 |
+
_ck_stochastic_rounding_fp8 = ck.stochastic_rounding_fp8
|
| 9 |
+
_CK_STOCHASTIC_ROUNDING_AVAILABLE = True
|
| 10 |
+
except (AttributeError, ImportError):
|
| 11 |
+
logging.warning("comfy_kitchen does not support stochastic FP8 rounding, please update comfy_kitchen.")
|
| 12 |
+
|
| 13 |
+
if not _CK_STOCHASTIC_ROUNDING_AVAILABLE:
|
| 14 |
+
def _ck_stochastic_rounding_fp8(value, rng, dtype):
|
| 15 |
+
raise NotImplementedError("comfy_kitchen does not support stochastic FP8 rounding")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def calc_mantissa(abs_x, exponent, normal_mask, MANTISSA_BITS, EXPONENT_BIAS, generator=None):
|
| 19 |
+
mantissa_scaled = torch.where(
|
| 20 |
+
normal_mask,
|
| 21 |
+
(abs_x / (2.0 ** (exponent - EXPONENT_BIAS)) - 1.0) * (2**MANTISSA_BITS),
|
| 22 |
+
(abs_x / (2.0 ** (-EXPONENT_BIAS + 1 - MANTISSA_BITS)))
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
mantissa_scaled += torch.rand(mantissa_scaled.size(), dtype=mantissa_scaled.dtype, layout=mantissa_scaled.layout, device=mantissa_scaled.device, generator=generator)
|
| 26 |
+
return mantissa_scaled.floor() / (2**MANTISSA_BITS)
|
| 27 |
+
|
| 28 |
+
#Not 100% sure about this
|
| 29 |
+
def manual_stochastic_round_to_float8(x, dtype, generator=None):
|
| 30 |
+
if dtype == torch.float8_e4m3fn:
|
| 31 |
+
EXPONENT_BITS, MANTISSA_BITS, EXPONENT_BIAS = 4, 3, 7
|
| 32 |
+
elif dtype == torch.float8_e5m2:
|
| 33 |
+
EXPONENT_BITS, MANTISSA_BITS, EXPONENT_BIAS = 5, 2, 15
|
| 34 |
+
else:
|
| 35 |
+
raise ValueError("Unsupported dtype")
|
| 36 |
+
|
| 37 |
+
x = x.half()
|
| 38 |
+
sign = torch.sign(x)
|
| 39 |
+
abs_x = x.abs()
|
| 40 |
+
sign = torch.where(abs_x == 0, 0, sign)
|
| 41 |
+
|
| 42 |
+
# Combine exponent calculation and clamping
|
| 43 |
+
exponent = torch.clamp(
|
| 44 |
+
torch.floor(torch.log2(abs_x)) + EXPONENT_BIAS,
|
| 45 |
+
0, 2**EXPONENT_BITS - 1
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Combine mantissa calculation and rounding
|
| 49 |
+
normal_mask = ~(exponent == 0)
|
| 50 |
+
|
| 51 |
+
abs_x[:] = calc_mantissa(abs_x, exponent, normal_mask, MANTISSA_BITS, EXPONENT_BIAS, generator=generator)
|
| 52 |
+
|
| 53 |
+
sign *= torch.where(
|
| 54 |
+
normal_mask,
|
| 55 |
+
(2.0 ** (exponent - EXPONENT_BIAS)) * (1.0 + abs_x),
|
| 56 |
+
(2.0 ** (-EXPONENT_BIAS + 1)) * abs_x
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
inf = torch.finfo(dtype)
|
| 60 |
+
torch.clamp(sign, min=inf.min, max=inf.max, out=sign)
|
| 61 |
+
return sign
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def stochastic_rounding(value, dtype, seed=0):
|
| 66 |
+
if dtype == torch.float32:
|
| 67 |
+
return value.to(dtype=torch.float32)
|
| 68 |
+
if dtype == torch.float16:
|
| 69 |
+
return value.to(dtype=torch.float16)
|
| 70 |
+
if dtype == torch.bfloat16:
|
| 71 |
+
return value.to(dtype=torch.bfloat16)
|
| 72 |
+
if dtype == torch.float8_e4m3fn or dtype == torch.float8_e5m2:
|
| 73 |
+
generator = torch.Generator(device=value.device)
|
| 74 |
+
generator.manual_seed(seed)
|
| 75 |
+
if _CK_STOCHASTIC_ROUNDING_AVAILABLE:
|
| 76 |
+
rng = torch.randint(0, 256, value.size(), dtype=torch.uint8, layout=value.layout, device=value.device, generator=generator)
|
| 77 |
+
return _ck_stochastic_rounding_fp8(value, rng, dtype)
|
| 78 |
+
|
| 79 |
+
output = torch.empty_like(value, dtype=dtype)
|
| 80 |
+
num_slices = max(1, (value.numel() / (4096 * 4096)))
|
| 81 |
+
slice_size = max(1, round(value.shape[0] / num_slices))
|
| 82 |
+
for i in range(0, value.shape[0], slice_size):
|
| 83 |
+
output[i:i+slice_size].copy_(manual_stochastic_round_to_float8(value[i:i+slice_size], dtype, generator=generator))
|
| 84 |
+
return output
|
| 85 |
+
|
| 86 |
+
return value.to(dtype=dtype)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# TODO: improve this?
|
| 90 |
+
def stochastic_float_to_fp4_e2m1(x, generator):
|
| 91 |
+
orig_shape = x.shape
|
| 92 |
+
sign = torch.signbit(x).to(torch.uint8)
|
| 93 |
+
|
| 94 |
+
exp = torch.floor(torch.log2(x.abs()) + 1.0).clamp(0, 3)
|
| 95 |
+
x += (torch.rand(x.size(), dtype=x.dtype, layout=x.layout, device=x.device, generator=generator) - 0.5) * (2 ** (exp - 2.0)) * 1.25
|
| 96 |
+
|
| 97 |
+
x = x.abs()
|
| 98 |
+
exp = torch.floor(torch.log2(x) + 1.1925).clamp(0, 3)
|
| 99 |
+
|
| 100 |
+
mantissa = torch.where(
|
| 101 |
+
exp > 0,
|
| 102 |
+
(x / (2.0 ** (exp - 1)) - 1.0) * 2.0,
|
| 103 |
+
(x * 2.0),
|
| 104 |
+
out=x
|
| 105 |
+
).round().to(torch.uint8)
|
| 106 |
+
del x
|
| 107 |
+
|
| 108 |
+
exp = exp.to(torch.uint8)
|
| 109 |
+
|
| 110 |
+
fp4 = (sign << 3) | (exp << 1) | mantissa
|
| 111 |
+
del sign, exp, mantissa
|
| 112 |
+
|
| 113 |
+
fp4_flat = fp4.view(-1)
|
| 114 |
+
packed = (fp4_flat[0::2] << 4) | fp4_flat[1::2]
|
| 115 |
+
return packed.reshape(list(orig_shape)[:-1] + [-1])
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def to_blocked(input_matrix, flatten: bool = True) -> torch.Tensor:
|
| 119 |
+
"""
|
| 120 |
+
Rearrange a large matrix by breaking it into blocks and applying the rearrangement pattern.
|
| 121 |
+
See:
|
| 122 |
+
https://docs.nvidia.com/cuda/cublas/index.html#d-block-scaling-factors-layout
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
input_matrix: Input tensor of shape (H, W)
|
| 126 |
+
Returns:
|
| 127 |
+
Rearranged tensor of shape (32*ceil_div(H,128), 16*ceil_div(W,4))
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
def ceil_div(a, b):
|
| 131 |
+
return (a + b - 1) // b
|
| 132 |
+
|
| 133 |
+
rows, cols = input_matrix.shape
|
| 134 |
+
n_row_blocks = ceil_div(rows, 128)
|
| 135 |
+
n_col_blocks = ceil_div(cols, 4)
|
| 136 |
+
|
| 137 |
+
# Calculate the padded shape
|
| 138 |
+
padded_rows = n_row_blocks * 128
|
| 139 |
+
padded_cols = n_col_blocks * 4
|
| 140 |
+
|
| 141 |
+
padded = input_matrix
|
| 142 |
+
if (rows, cols) != (padded_rows, padded_cols):
|
| 143 |
+
padded = torch.zeros(
|
| 144 |
+
(padded_rows, padded_cols),
|
| 145 |
+
device=input_matrix.device,
|
| 146 |
+
dtype=input_matrix.dtype,
|
| 147 |
+
)
|
| 148 |
+
padded[:rows, :cols] = input_matrix
|
| 149 |
+
|
| 150 |
+
# Rearrange the blocks
|
| 151 |
+
blocks = padded.view(n_row_blocks, 128, n_col_blocks, 4).permute(0, 2, 1, 3)
|
| 152 |
+
rearranged = blocks.reshape(-1, 4, 32, 4).transpose(1, 2).reshape(-1, 32, 16)
|
| 153 |
+
if flatten:
|
| 154 |
+
return rearranged.flatten()
|
| 155 |
+
|
| 156 |
+
return rearranged.reshape(padded_rows, padded_cols)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def stochastic_round_quantize_nvfp4_block(x, per_tensor_scale, generator):
|
| 160 |
+
F4_E2M1_MAX = 6.0
|
| 161 |
+
F8_E4M3_MAX = 448.0
|
| 162 |
+
|
| 163 |
+
orig_shape = x.shape
|
| 164 |
+
|
| 165 |
+
block_size = 16
|
| 166 |
+
|
| 167 |
+
x = x.reshape(orig_shape[0], -1, block_size)
|
| 168 |
+
scaled_block_scales_fp8 = torch.clamp(((torch.amax(torch.abs(x), dim=-1)) / F4_E2M1_MAX) / per_tensor_scale.to(x.dtype), max=F8_E4M3_MAX).to(torch.float8_e4m3fn)
|
| 169 |
+
x = x / (per_tensor_scale.to(x.dtype) * scaled_block_scales_fp8.to(x.dtype)).unsqueeze(-1)
|
| 170 |
+
|
| 171 |
+
x = x.view(orig_shape).nan_to_num()
|
| 172 |
+
data_lp = stochastic_float_to_fp4_e2m1(x, generator=generator)
|
| 173 |
+
return data_lp, scaled_block_scales_fp8
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def stochastic_round_quantize_nvfp4(x, per_tensor_scale, pad_16x, seed=0):
|
| 177 |
+
def roundup(x: int, multiple: int) -> int:
|
| 178 |
+
"""Round up x to the nearest multiple."""
|
| 179 |
+
return ((x + multiple - 1) // multiple) * multiple
|
| 180 |
+
|
| 181 |
+
generator = torch.Generator(device=x.device)
|
| 182 |
+
generator.manual_seed(seed)
|
| 183 |
+
|
| 184 |
+
# Handle padding
|
| 185 |
+
if pad_16x:
|
| 186 |
+
rows, cols = x.shape
|
| 187 |
+
padded_rows = roundup(rows, 16)
|
| 188 |
+
padded_cols = roundup(cols, 16)
|
| 189 |
+
if padded_rows != rows or padded_cols != cols:
|
| 190 |
+
x = torch.nn.functional.pad(x, (0, padded_cols - cols, 0, padded_rows - rows))
|
| 191 |
+
|
| 192 |
+
x, blocked_scaled = stochastic_round_quantize_nvfp4_block(x, per_tensor_scale, generator)
|
| 193 |
+
return x, to_blocked(blocked_scaled, flatten=False)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def stochastic_round_quantize_nvfp4_by_block(x, per_tensor_scale, pad_16x, seed=0, block_size=4096 * 4096):
|
| 197 |
+
def roundup(x: int, multiple: int) -> int:
|
| 198 |
+
"""Round up x to the nearest multiple."""
|
| 199 |
+
return ((x + multiple - 1) // multiple) * multiple
|
| 200 |
+
|
| 201 |
+
orig_shape = x.shape
|
| 202 |
+
|
| 203 |
+
# Handle padding
|
| 204 |
+
if pad_16x:
|
| 205 |
+
rows, cols = x.shape
|
| 206 |
+
padded_rows = roundup(rows, 16)
|
| 207 |
+
padded_cols = roundup(cols, 16)
|
| 208 |
+
if padded_rows != rows or padded_cols != cols:
|
| 209 |
+
x = torch.nn.functional.pad(x, (0, padded_cols - cols, 0, padded_rows - rows))
|
| 210 |
+
# Note: We update orig_shape because the output tensor logic below assumes x.shape matches
|
| 211 |
+
# what we want to produce. If we pad here, we want the padded output.
|
| 212 |
+
orig_shape = x.shape
|
| 213 |
+
|
| 214 |
+
orig_shape = list(orig_shape)
|
| 215 |
+
|
| 216 |
+
output_fp4 = torch.empty(orig_shape[:-1] + [orig_shape[-1] // 2], dtype=torch.uint8, device=x.device)
|
| 217 |
+
output_block = torch.empty(orig_shape[:-1] + [orig_shape[-1] // 16], dtype=torch.float8_e4m3fn, device=x.device)
|
| 218 |
+
|
| 219 |
+
generator = torch.Generator(device=x.device)
|
| 220 |
+
generator.manual_seed(seed)
|
| 221 |
+
|
| 222 |
+
num_slices = max(1, (x.numel() / block_size))
|
| 223 |
+
slice_size = max(1, (round(x.shape[0] / num_slices)))
|
| 224 |
+
|
| 225 |
+
for i in range(0, x.shape[0], slice_size):
|
| 226 |
+
fp4, block = stochastic_round_quantize_nvfp4_block(x[i: i + slice_size], per_tensor_scale, generator=generator)
|
| 227 |
+
output_fp4[i:i + slice_size].copy_(fp4)
|
| 228 |
+
output_block[i:i + slice_size].copy_(block)
|
| 229 |
+
|
| 230 |
+
return output_fp4, to_blocked(output_block, flatten=False)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def stochastic_round_quantize_mxfp8_by_block(x, pad_32x, seed=0):
|
| 234 |
+
def roundup(x_val, multiple):
|
| 235 |
+
return ((x_val + multiple - 1) // multiple) * multiple
|
| 236 |
+
|
| 237 |
+
if pad_32x:
|
| 238 |
+
rows, cols = x.shape
|
| 239 |
+
padded_rows = roundup(rows, 32)
|
| 240 |
+
padded_cols = roundup(cols, 32)
|
| 241 |
+
if padded_rows != rows or padded_cols != cols:
|
| 242 |
+
x = torch.nn.functional.pad(x, (0, padded_cols - cols, 0, padded_rows - rows))
|
| 243 |
+
|
| 244 |
+
F8_E4M3_MAX = 448.0
|
| 245 |
+
E8M0_BIAS = 127
|
| 246 |
+
BLOCK_SIZE = 32
|
| 247 |
+
|
| 248 |
+
rows, cols = x.shape
|
| 249 |
+
x_blocked = x.reshape(rows, -1, BLOCK_SIZE)
|
| 250 |
+
max_abs = torch.amax(torch.abs(x_blocked), dim=-1)
|
| 251 |
+
|
| 252 |
+
# E8M0 block scales (power-of-2 exponents)
|
| 253 |
+
scale_needed = torch.clamp(max_abs.float() / F8_E4M3_MAX, min=2**(-127))
|
| 254 |
+
exp_biased = torch.clamp(torch.ceil(torch.log2(scale_needed)).to(torch.int32) + E8M0_BIAS, 0, 254)
|
| 255 |
+
block_scales_e8m0 = exp_biased.to(torch.uint8)
|
| 256 |
+
|
| 257 |
+
zero_mask = (max_abs == 0)
|
| 258 |
+
block_scales_f32 = (block_scales_e8m0.to(torch.int32) << 23).view(torch.float32)
|
| 259 |
+
block_scales_f32 = torch.where(zero_mask, torch.ones_like(block_scales_f32), block_scales_f32)
|
| 260 |
+
|
| 261 |
+
# Scale per-block then stochastic round
|
| 262 |
+
data_scaled = (x_blocked.float() / block_scales_f32.unsqueeze(-1)).reshape(rows, cols)
|
| 263 |
+
output_fp8 = stochastic_rounding(data_scaled, torch.float8_e4m3fn, seed=seed)
|
| 264 |
+
|
| 265 |
+
block_scales_e8m0 = torch.where(zero_mask, torch.zeros_like(block_scales_e8m0), block_scales_e8m0)
|
| 266 |
+
return output_fp8, to_blocked(block_scales_e8m0, flatten=False).view(torch.float8_e8m0fnu)
|
comfy/gligen.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch
|
| 3 |
+
from torch import nn
|
| 4 |
+
from .ldm.modules.attention import CrossAttention, FeedForward
|
| 5 |
+
import comfy.ops
|
| 6 |
+
ops = comfy.ops.manual_cast
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class GatedCrossAttentionDense(nn.Module):
|
| 10 |
+
def __init__(self, query_dim, context_dim, n_heads, d_head):
|
| 11 |
+
super().__init__()
|
| 12 |
+
|
| 13 |
+
self.attn = CrossAttention(
|
| 14 |
+
query_dim=query_dim,
|
| 15 |
+
context_dim=context_dim,
|
| 16 |
+
heads=n_heads,
|
| 17 |
+
dim_head=d_head,
|
| 18 |
+
operations=ops)
|
| 19 |
+
self.ff = FeedForward(query_dim, glu=True)
|
| 20 |
+
|
| 21 |
+
self.norm1 = ops.LayerNorm(query_dim)
|
| 22 |
+
self.norm2 = ops.LayerNorm(query_dim)
|
| 23 |
+
|
| 24 |
+
self.register_parameter('alpha_attn', nn.Parameter(torch.tensor(0.)))
|
| 25 |
+
self.register_parameter('alpha_dense', nn.Parameter(torch.tensor(0.)))
|
| 26 |
+
|
| 27 |
+
# this can be useful: we can externally change magnitude of tanh(alpha)
|
| 28 |
+
# for example, when it is set to 0, then the entire model is same as
|
| 29 |
+
# original one
|
| 30 |
+
self.scale = 1
|
| 31 |
+
|
| 32 |
+
def forward(self, x, objs):
|
| 33 |
+
|
| 34 |
+
x = x + self.scale * \
|
| 35 |
+
torch.tanh(self.alpha_attn) * self.attn(self.norm1(x), objs, objs)
|
| 36 |
+
x = x + self.scale * \
|
| 37 |
+
torch.tanh(self.alpha_dense) * self.ff(self.norm2(x))
|
| 38 |
+
|
| 39 |
+
return x
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class GatedSelfAttentionDense(nn.Module):
|
| 43 |
+
def __init__(self, query_dim, context_dim, n_heads, d_head):
|
| 44 |
+
super().__init__()
|
| 45 |
+
|
| 46 |
+
# we need a linear projection since we need cat visual feature and obj
|
| 47 |
+
# feature
|
| 48 |
+
self.linear = ops.Linear(context_dim, query_dim)
|
| 49 |
+
|
| 50 |
+
self.attn = CrossAttention(
|
| 51 |
+
query_dim=query_dim,
|
| 52 |
+
context_dim=query_dim,
|
| 53 |
+
heads=n_heads,
|
| 54 |
+
dim_head=d_head,
|
| 55 |
+
operations=ops)
|
| 56 |
+
self.ff = FeedForward(query_dim, glu=True)
|
| 57 |
+
|
| 58 |
+
self.norm1 = ops.LayerNorm(query_dim)
|
| 59 |
+
self.norm2 = ops.LayerNorm(query_dim)
|
| 60 |
+
|
| 61 |
+
self.register_parameter('alpha_attn', nn.Parameter(torch.tensor(0.)))
|
| 62 |
+
self.register_parameter('alpha_dense', nn.Parameter(torch.tensor(0.)))
|
| 63 |
+
|
| 64 |
+
# this can be useful: we can externally change magnitude of tanh(alpha)
|
| 65 |
+
# for example, when it is set to 0, then the entire model is same as
|
| 66 |
+
# original one
|
| 67 |
+
self.scale = 1
|
| 68 |
+
|
| 69 |
+
def forward(self, x, objs):
|
| 70 |
+
|
| 71 |
+
N_visual = x.shape[1]
|
| 72 |
+
objs = self.linear(objs)
|
| 73 |
+
|
| 74 |
+
x = x + self.scale * torch.tanh(self.alpha_attn) * self.attn(
|
| 75 |
+
self.norm1(torch.cat([x, objs], dim=1)))[:, 0:N_visual, :]
|
| 76 |
+
x = x + self.scale * \
|
| 77 |
+
torch.tanh(self.alpha_dense) * self.ff(self.norm2(x))
|
| 78 |
+
|
| 79 |
+
return x
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class GatedSelfAttentionDense2(nn.Module):
|
| 83 |
+
def __init__(self, query_dim, context_dim, n_heads, d_head):
|
| 84 |
+
super().__init__()
|
| 85 |
+
|
| 86 |
+
# we need a linear projection since we need cat visual feature and obj
|
| 87 |
+
# feature
|
| 88 |
+
self.linear = ops.Linear(context_dim, query_dim)
|
| 89 |
+
|
| 90 |
+
self.attn = CrossAttention(
|
| 91 |
+
query_dim=query_dim, context_dim=query_dim, dim_head=d_head, operations=ops)
|
| 92 |
+
self.ff = FeedForward(query_dim, glu=True)
|
| 93 |
+
|
| 94 |
+
self.norm1 = ops.LayerNorm(query_dim)
|
| 95 |
+
self.norm2 = ops.LayerNorm(query_dim)
|
| 96 |
+
|
| 97 |
+
self.register_parameter('alpha_attn', nn.Parameter(torch.tensor(0.)))
|
| 98 |
+
self.register_parameter('alpha_dense', nn.Parameter(torch.tensor(0.)))
|
| 99 |
+
|
| 100 |
+
# this can be useful: we can externally change magnitude of tanh(alpha)
|
| 101 |
+
# for example, when it is set to 0, then the entire model is same as
|
| 102 |
+
# original one
|
| 103 |
+
self.scale = 1
|
| 104 |
+
|
| 105 |
+
def forward(self, x, objs):
|
| 106 |
+
|
| 107 |
+
B, N_visual, _ = x.shape
|
| 108 |
+
B, N_ground, _ = objs.shape
|
| 109 |
+
|
| 110 |
+
objs = self.linear(objs)
|
| 111 |
+
|
| 112 |
+
# sanity check
|
| 113 |
+
size_v = math.sqrt(N_visual)
|
| 114 |
+
size_g = math.sqrt(N_ground)
|
| 115 |
+
assert int(size_v) == size_v, "Visual tokens must be square rootable"
|
| 116 |
+
assert int(size_g) == size_g, "Grounding tokens must be square rootable"
|
| 117 |
+
size_v = int(size_v)
|
| 118 |
+
size_g = int(size_g)
|
| 119 |
+
|
| 120 |
+
# select grounding token and resize it to visual token size as residual
|
| 121 |
+
out = self.attn(self.norm1(torch.cat([x, objs], dim=1)))[
|
| 122 |
+
:, N_visual:, :]
|
| 123 |
+
out = out.permute(0, 2, 1).reshape(B, -1, size_g, size_g)
|
| 124 |
+
out = torch.nn.functional.interpolate(
|
| 125 |
+
out, (size_v, size_v), mode='bicubic')
|
| 126 |
+
residual = out.reshape(B, -1, N_visual).permute(0, 2, 1)
|
| 127 |
+
|
| 128 |
+
# add residual to visual feature
|
| 129 |
+
x = x + self.scale * torch.tanh(self.alpha_attn) * residual
|
| 130 |
+
x = x + self.scale * \
|
| 131 |
+
torch.tanh(self.alpha_dense) * self.ff(self.norm2(x))
|
| 132 |
+
|
| 133 |
+
return x
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
class FourierEmbedder():
|
| 137 |
+
def __init__(self, num_freqs=64, temperature=100):
|
| 138 |
+
|
| 139 |
+
self.num_freqs = num_freqs
|
| 140 |
+
self.temperature = temperature
|
| 141 |
+
self.freq_bands = temperature ** (torch.arange(num_freqs) / num_freqs)
|
| 142 |
+
|
| 143 |
+
@torch.no_grad()
|
| 144 |
+
def __call__(self, x, cat_dim=-1):
|
| 145 |
+
"x: arbitrary shape of tensor. dim: cat dim"
|
| 146 |
+
out = []
|
| 147 |
+
for freq in self.freq_bands:
|
| 148 |
+
out.append(torch.sin(freq * x))
|
| 149 |
+
out.append(torch.cos(freq * x))
|
| 150 |
+
return torch.cat(out, cat_dim)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class PositionNet(nn.Module):
|
| 154 |
+
def __init__(self, in_dim, out_dim, fourier_freqs=8):
|
| 155 |
+
super().__init__()
|
| 156 |
+
self.in_dim = in_dim
|
| 157 |
+
self.out_dim = out_dim
|
| 158 |
+
|
| 159 |
+
self.fourier_embedder = FourierEmbedder(num_freqs=fourier_freqs)
|
| 160 |
+
self.position_dim = fourier_freqs * 2 * 4 # 2 is sin&cos, 4 is xyxy
|
| 161 |
+
|
| 162 |
+
self.linears = nn.Sequential(
|
| 163 |
+
ops.Linear(self.in_dim + self.position_dim, 512),
|
| 164 |
+
nn.SiLU(),
|
| 165 |
+
ops.Linear(512, 512),
|
| 166 |
+
nn.SiLU(),
|
| 167 |
+
ops.Linear(512, out_dim),
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
self.null_positive_feature = torch.nn.Parameter(
|
| 171 |
+
torch.zeros([self.in_dim]))
|
| 172 |
+
self.null_position_feature = torch.nn.Parameter(
|
| 173 |
+
torch.zeros([self.position_dim]))
|
| 174 |
+
|
| 175 |
+
def forward(self, boxes, masks, positive_embeddings):
|
| 176 |
+
B, N, _ = boxes.shape
|
| 177 |
+
masks = masks.unsqueeze(-1)
|
| 178 |
+
positive_embeddings = positive_embeddings
|
| 179 |
+
|
| 180 |
+
# embedding position (it may includes padding as placeholder)
|
| 181 |
+
xyxy_embedding = self.fourier_embedder(boxes) # B*N*4 --> B*N*C
|
| 182 |
+
|
| 183 |
+
# learnable null embedding
|
| 184 |
+
positive_null = self.null_positive_feature.to(device=boxes.device, dtype=boxes.dtype).view(1, 1, -1)
|
| 185 |
+
xyxy_null = self.null_position_feature.to(device=boxes.device, dtype=boxes.dtype).view(1, 1, -1)
|
| 186 |
+
|
| 187 |
+
# replace padding with learnable null embedding
|
| 188 |
+
positive_embeddings = positive_embeddings * \
|
| 189 |
+
masks + (1 - masks) * positive_null
|
| 190 |
+
xyxy_embedding = xyxy_embedding * masks + (1 - masks) * xyxy_null
|
| 191 |
+
|
| 192 |
+
objs = self.linears(
|
| 193 |
+
torch.cat([positive_embeddings, xyxy_embedding], dim=-1))
|
| 194 |
+
assert objs.shape == torch.Size([B, N, self.out_dim])
|
| 195 |
+
return objs
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class Gligen(nn.Module):
|
| 199 |
+
def __init__(self, modules, position_net, key_dim):
|
| 200 |
+
super().__init__()
|
| 201 |
+
self.module_list = nn.ModuleList(modules)
|
| 202 |
+
self.position_net = position_net
|
| 203 |
+
self.key_dim = key_dim
|
| 204 |
+
self.max_objs = 30
|
| 205 |
+
self.current_device = torch.device("cpu")
|
| 206 |
+
|
| 207 |
+
def _set_position(self, boxes, masks, positive_embeddings):
|
| 208 |
+
objs = self.position_net(boxes, masks, positive_embeddings)
|
| 209 |
+
def func(x, extra_options):
|
| 210 |
+
key = extra_options["transformer_index"]
|
| 211 |
+
module = self.module_list[key]
|
| 212 |
+
return module(x, objs.to(device=x.device, dtype=x.dtype))
|
| 213 |
+
return func
|
| 214 |
+
|
| 215 |
+
def set_position(self, latent_image_shape, position_params, device):
|
| 216 |
+
batch, c, h, w = latent_image_shape
|
| 217 |
+
masks = torch.zeros([self.max_objs], device="cpu")
|
| 218 |
+
boxes = []
|
| 219 |
+
positive_embeddings = []
|
| 220 |
+
for p in position_params:
|
| 221 |
+
x1 = (p[4]) / w
|
| 222 |
+
y1 = (p[3]) / h
|
| 223 |
+
x2 = (p[4] + p[2]) / w
|
| 224 |
+
y2 = (p[3] + p[1]) / h
|
| 225 |
+
masks[len(boxes)] = 1.0
|
| 226 |
+
boxes += [torch.tensor((x1, y1, x2, y2)).unsqueeze(0)]
|
| 227 |
+
positive_embeddings += [p[0]]
|
| 228 |
+
append_boxes = []
|
| 229 |
+
append_conds = []
|
| 230 |
+
if len(boxes) < self.max_objs:
|
| 231 |
+
append_boxes = [torch.zeros(
|
| 232 |
+
[self.max_objs - len(boxes), 4], device="cpu")]
|
| 233 |
+
append_conds = [torch.zeros(
|
| 234 |
+
[self.max_objs - len(boxes), self.key_dim], device="cpu")]
|
| 235 |
+
|
| 236 |
+
box_out = torch.cat(
|
| 237 |
+
boxes + append_boxes).unsqueeze(0).repeat(batch, 1, 1)
|
| 238 |
+
masks = masks.unsqueeze(0).repeat(batch, 1)
|
| 239 |
+
conds = torch.cat(positive_embeddings +
|
| 240 |
+
append_conds).unsqueeze(0).repeat(batch, 1, 1)
|
| 241 |
+
return self._set_position(
|
| 242 |
+
box_out.to(device),
|
| 243 |
+
masks.to(device),
|
| 244 |
+
conds.to(device))
|
| 245 |
+
|
| 246 |
+
def set_empty(self, latent_image_shape, device):
|
| 247 |
+
batch, c, h, w = latent_image_shape
|
| 248 |
+
masks = torch.zeros([self.max_objs], device="cpu").repeat(batch, 1)
|
| 249 |
+
box_out = torch.zeros([self.max_objs, 4],
|
| 250 |
+
device="cpu").repeat(batch, 1, 1)
|
| 251 |
+
conds = torch.zeros([self.max_objs, self.key_dim],
|
| 252 |
+
device="cpu").repeat(batch, 1, 1)
|
| 253 |
+
return self._set_position(
|
| 254 |
+
box_out.to(device),
|
| 255 |
+
masks.to(device),
|
| 256 |
+
conds.to(device))
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def load_gligen(sd):
|
| 260 |
+
sd_k = sd.keys()
|
| 261 |
+
output_list = []
|
| 262 |
+
key_dim = 768
|
| 263 |
+
for a in ["input_blocks", "middle_block", "output_blocks"]:
|
| 264 |
+
for b in range(20):
|
| 265 |
+
k_temp = filter(lambda k: "{}.{}.".format(a, b)
|
| 266 |
+
in k and ".fuser." in k, sd_k)
|
| 267 |
+
k_temp = map(lambda k: (k, k.split(".fuser.")[-1]), k_temp)
|
| 268 |
+
|
| 269 |
+
n_sd = {}
|
| 270 |
+
for k in k_temp:
|
| 271 |
+
n_sd[k[1]] = sd[k[0]]
|
| 272 |
+
if len(n_sd) > 0:
|
| 273 |
+
query_dim = n_sd["linear.weight"].shape[0]
|
| 274 |
+
key_dim = n_sd["linear.weight"].shape[1]
|
| 275 |
+
|
| 276 |
+
if key_dim == 768: # SD1.x
|
| 277 |
+
n_heads = 8
|
| 278 |
+
d_head = query_dim // n_heads
|
| 279 |
+
else:
|
| 280 |
+
d_head = 64
|
| 281 |
+
n_heads = query_dim // d_head
|
| 282 |
+
|
| 283 |
+
gated = GatedSelfAttentionDense(
|
| 284 |
+
query_dim, key_dim, n_heads, d_head)
|
| 285 |
+
gated.load_state_dict(n_sd, strict=False)
|
| 286 |
+
output_list.append(gated)
|
| 287 |
+
|
| 288 |
+
if "position_net.null_positive_feature" in sd_k:
|
| 289 |
+
in_dim = sd["position_net.null_positive_feature"].shape[0]
|
| 290 |
+
out_dim = sd["position_net.linears.4.weight"].shape[0]
|
| 291 |
+
|
| 292 |
+
class WeightsLoader(torch.nn.Module):
|
| 293 |
+
pass
|
| 294 |
+
w = WeightsLoader()
|
| 295 |
+
w.position_net = PositionNet(in_dim, out_dim)
|
| 296 |
+
w.load_state_dict(sd, strict=False)
|
| 297 |
+
|
| 298 |
+
gligen = Gligen(output_list, w.position_net, key_dim)
|
| 299 |
+
return gligen
|
comfy/hooks.py
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from typing import TYPE_CHECKING, Callable
|
| 3 |
+
import enum
|
| 4 |
+
import math
|
| 5 |
+
import torch
|
| 6 |
+
import numpy as np
|
| 7 |
+
import itertools
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
if TYPE_CHECKING:
|
| 11 |
+
from comfy.model_patcher import ModelPatcher, PatcherInjection
|
| 12 |
+
from comfy.model_base import BaseModel
|
| 13 |
+
from comfy.sd import CLIP
|
| 14 |
+
import comfy.lora
|
| 15 |
+
import comfy.model_management
|
| 16 |
+
import comfy.patcher_extension
|
| 17 |
+
from node_helpers import conditioning_set_values
|
| 18 |
+
|
| 19 |
+
# #######################################################################################################
|
| 20 |
+
# Hooks explanation
|
| 21 |
+
# -------------------
|
| 22 |
+
# The purpose of hooks is to allow conds to influence sampling without the need for ComfyUI core code to
|
| 23 |
+
# make explicit special cases like it does for ControlNet and GLIGEN.
|
| 24 |
+
#
|
| 25 |
+
# This is necessary for nodes/features that are intended for use with masked or scheduled conds, or those
|
| 26 |
+
# that should run special code when a 'marked' cond is used in sampling.
|
| 27 |
+
# #######################################################################################################
|
| 28 |
+
|
| 29 |
+
class EnumHookMode(enum.Enum):
|
| 30 |
+
'''
|
| 31 |
+
Priority of hook memory optimization vs. speed, mostly related to WeightHooks.
|
| 32 |
+
|
| 33 |
+
MinVram: No caching will occur for any operations related to hooks.
|
| 34 |
+
MaxSpeed: Excess VRAM (and RAM, once VRAM is sufficiently depleted) will be used to cache hook weights when switching hook groups.
|
| 35 |
+
'''
|
| 36 |
+
MinVram = "minvram"
|
| 37 |
+
MaxSpeed = "maxspeed"
|
| 38 |
+
|
| 39 |
+
class EnumHookType(enum.Enum):
|
| 40 |
+
'''
|
| 41 |
+
Hook types, each of which has different expected behavior.
|
| 42 |
+
'''
|
| 43 |
+
Weight = "weight"
|
| 44 |
+
ObjectPatch = "object_patch"
|
| 45 |
+
AdditionalModels = "add_models"
|
| 46 |
+
TransformerOptions = "transformer_options"
|
| 47 |
+
Injections = "add_injections"
|
| 48 |
+
|
| 49 |
+
class EnumWeightTarget(enum.Enum):
|
| 50 |
+
Model = "model"
|
| 51 |
+
Clip = "clip"
|
| 52 |
+
|
| 53 |
+
class EnumHookScope(enum.Enum):
|
| 54 |
+
'''
|
| 55 |
+
Determines if hook should be limited in its influence over sampling.
|
| 56 |
+
|
| 57 |
+
AllConditioning: hook will affect all conds used in sampling.
|
| 58 |
+
HookedOnly: hook will only affect the conds it was attached to.
|
| 59 |
+
'''
|
| 60 |
+
AllConditioning = "all_conditioning"
|
| 61 |
+
HookedOnly = "hooked_only"
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class _HookRef:
|
| 65 |
+
pass
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def default_should_register(hook: Hook, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):
|
| 69 |
+
'''Example for how custom_should_register function can look like.'''
|
| 70 |
+
return True
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def create_target_dict(target: EnumWeightTarget=None, **kwargs) -> dict[str]:
|
| 74 |
+
'''Creates base dictionary for use with Hooks' target param.'''
|
| 75 |
+
d = {}
|
| 76 |
+
if target is not None:
|
| 77 |
+
d['target'] = target
|
| 78 |
+
d.update(kwargs)
|
| 79 |
+
return d
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class Hook:
|
| 83 |
+
def __init__(self, hook_type: EnumHookType=None, hook_ref: _HookRef=None, hook_id: str=None,
|
| 84 |
+
hook_keyframe: HookKeyframeGroup=None, hook_scope=EnumHookScope.AllConditioning):
|
| 85 |
+
self.hook_type = hook_type
|
| 86 |
+
'''Enum identifying the general class of this hook.'''
|
| 87 |
+
self.hook_ref = hook_ref if hook_ref else _HookRef()
|
| 88 |
+
'''Reference shared between hook clones that have the same value. Should NOT be modified.'''
|
| 89 |
+
self.hook_id = hook_id
|
| 90 |
+
'''Optional string ID to identify hook; useful if need to consolidate duplicates at registration time.'''
|
| 91 |
+
self.hook_keyframe = hook_keyframe if hook_keyframe else HookKeyframeGroup()
|
| 92 |
+
'''Keyframe storage that can be referenced to get strength for current sampling step.'''
|
| 93 |
+
self.hook_scope = hook_scope
|
| 94 |
+
'''Scope of where this hook should apply in terms of the conds used in sampling run.'''
|
| 95 |
+
self.custom_should_register = default_should_register
|
| 96 |
+
'''Can be overridden with a compatible function to decide if this hook should be registered without the need to override .should_register'''
|
| 97 |
+
|
| 98 |
+
@property
|
| 99 |
+
def strength(self):
|
| 100 |
+
return self.hook_keyframe.strength
|
| 101 |
+
|
| 102 |
+
def initialize_timesteps(self, model: BaseModel):
|
| 103 |
+
self.reset()
|
| 104 |
+
self.hook_keyframe.initialize_timesteps(model)
|
| 105 |
+
|
| 106 |
+
def reset(self):
|
| 107 |
+
self.hook_keyframe.reset()
|
| 108 |
+
|
| 109 |
+
def clone(self):
|
| 110 |
+
c: Hook = self.__class__()
|
| 111 |
+
c.hook_type = self.hook_type
|
| 112 |
+
c.hook_ref = self.hook_ref
|
| 113 |
+
c.hook_id = self.hook_id
|
| 114 |
+
c.hook_keyframe = self.hook_keyframe
|
| 115 |
+
c.hook_scope = self.hook_scope
|
| 116 |
+
c.custom_should_register = self.custom_should_register
|
| 117 |
+
return c
|
| 118 |
+
|
| 119 |
+
def should_register(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):
|
| 120 |
+
return self.custom_should_register(self, model, model_options, target_dict, registered)
|
| 121 |
+
|
| 122 |
+
def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):
|
| 123 |
+
raise NotImplementedError("add_hook_patches should be defined for Hook subclasses")
|
| 124 |
+
|
| 125 |
+
def __eq__(self, other: Hook):
|
| 126 |
+
return self.__class__ == other.__class__ and self.hook_ref == other.hook_ref
|
| 127 |
+
|
| 128 |
+
def __hash__(self):
|
| 129 |
+
return hash(self.hook_ref)
|
| 130 |
+
|
| 131 |
+
class WeightHook(Hook):
|
| 132 |
+
'''
|
| 133 |
+
Hook responsible for tracking weights to be applied to some model/clip.
|
| 134 |
+
|
| 135 |
+
Note, value of hook_scope is ignored and is treated as HookedOnly.
|
| 136 |
+
'''
|
| 137 |
+
def __init__(self, strength_model=1.0, strength_clip=1.0):
|
| 138 |
+
super().__init__(hook_type=EnumHookType.Weight, hook_scope=EnumHookScope.HookedOnly)
|
| 139 |
+
self.weights: dict = None
|
| 140 |
+
self.weights_clip: dict = None
|
| 141 |
+
self.need_weight_init = True
|
| 142 |
+
self._strength_model = strength_model
|
| 143 |
+
self._strength_clip = strength_clip
|
| 144 |
+
self.hook_scope = EnumHookScope.HookedOnly # this value does not matter for WeightHooks, just for docs
|
| 145 |
+
|
| 146 |
+
@property
|
| 147 |
+
def strength_model(self):
|
| 148 |
+
return self._strength_model * self.strength
|
| 149 |
+
|
| 150 |
+
@property
|
| 151 |
+
def strength_clip(self):
|
| 152 |
+
return self._strength_clip * self.strength
|
| 153 |
+
|
| 154 |
+
def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):
|
| 155 |
+
if not self.should_register(model, model_options, target_dict, registered):
|
| 156 |
+
return False
|
| 157 |
+
weights = None
|
| 158 |
+
|
| 159 |
+
target = target_dict.get('target', None)
|
| 160 |
+
if target == EnumWeightTarget.Clip:
|
| 161 |
+
strength = self._strength_clip
|
| 162 |
+
else:
|
| 163 |
+
strength = self._strength_model
|
| 164 |
+
|
| 165 |
+
if self.need_weight_init:
|
| 166 |
+
key_map = {}
|
| 167 |
+
if target == EnumWeightTarget.Clip:
|
| 168 |
+
key_map = comfy.lora.model_lora_keys_clip(model.model, key_map)
|
| 169 |
+
else:
|
| 170 |
+
key_map = comfy.lora.model_lora_keys_unet(model.model, key_map)
|
| 171 |
+
weights = comfy.lora.load_lora(self.weights, key_map, log_missing=False)
|
| 172 |
+
else:
|
| 173 |
+
if target == EnumWeightTarget.Clip:
|
| 174 |
+
weights = self.weights_clip
|
| 175 |
+
else:
|
| 176 |
+
weights = self.weights
|
| 177 |
+
model.add_hook_patches(hook=self, patches=weights, strength_patch=strength)
|
| 178 |
+
registered.add(self)
|
| 179 |
+
return True
|
| 180 |
+
# TODO: add logs about any keys that were not applied
|
| 181 |
+
|
| 182 |
+
def clone(self):
|
| 183 |
+
c: WeightHook = super().clone()
|
| 184 |
+
c.weights = self.weights
|
| 185 |
+
c.weights_clip = self.weights_clip
|
| 186 |
+
c.need_weight_init = self.need_weight_init
|
| 187 |
+
c._strength_model = self._strength_model
|
| 188 |
+
c._strength_clip = self._strength_clip
|
| 189 |
+
return c
|
| 190 |
+
|
| 191 |
+
class ObjectPatchHook(Hook):
|
| 192 |
+
def __init__(self, object_patches: dict[str]=None,
|
| 193 |
+
hook_scope=EnumHookScope.AllConditioning):
|
| 194 |
+
super().__init__(hook_type=EnumHookType.ObjectPatch)
|
| 195 |
+
self.object_patches = object_patches
|
| 196 |
+
self.hook_scope = hook_scope
|
| 197 |
+
|
| 198 |
+
def clone(self):
|
| 199 |
+
c: ObjectPatchHook = super().clone()
|
| 200 |
+
c.object_patches = self.object_patches
|
| 201 |
+
return c
|
| 202 |
+
|
| 203 |
+
def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):
|
| 204 |
+
raise NotImplementedError("ObjectPatchHook is not supported yet in ComfyUI.")
|
| 205 |
+
|
| 206 |
+
class AdditionalModelsHook(Hook):
|
| 207 |
+
'''
|
| 208 |
+
Hook responsible for telling model management any additional models that should be loaded.
|
| 209 |
+
|
| 210 |
+
Note, value of hook_scope is ignored and is treated as AllConditioning.
|
| 211 |
+
'''
|
| 212 |
+
def __init__(self, models: list[ModelPatcher]=None, key: str=None):
|
| 213 |
+
super().__init__(hook_type=EnumHookType.AdditionalModels)
|
| 214 |
+
self.models = models
|
| 215 |
+
self.key = key
|
| 216 |
+
|
| 217 |
+
def clone(self):
|
| 218 |
+
c: AdditionalModelsHook = super().clone()
|
| 219 |
+
c.models = self.models.copy() if self.models else self.models
|
| 220 |
+
c.key = self.key
|
| 221 |
+
return c
|
| 222 |
+
|
| 223 |
+
def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):
|
| 224 |
+
if not self.should_register(model, model_options, target_dict, registered):
|
| 225 |
+
return False
|
| 226 |
+
registered.add(self)
|
| 227 |
+
return True
|
| 228 |
+
|
| 229 |
+
class TransformerOptionsHook(Hook):
|
| 230 |
+
'''
|
| 231 |
+
Hook responsible for adding wrappers, callbacks, patches, or anything else related to transformer_options.
|
| 232 |
+
'''
|
| 233 |
+
def __init__(self, transformers_dict: dict[str, dict[str, dict[str, list[Callable]]]]=None,
|
| 234 |
+
hook_scope=EnumHookScope.AllConditioning):
|
| 235 |
+
super().__init__(hook_type=EnumHookType.TransformerOptions)
|
| 236 |
+
self.transformers_dict = transformers_dict
|
| 237 |
+
self.hook_scope = hook_scope
|
| 238 |
+
self._skip_adding = False
|
| 239 |
+
'''Internal value used to avoid double load of transformer_options when hook_scope is AllConditioning.'''
|
| 240 |
+
|
| 241 |
+
def clone(self):
|
| 242 |
+
c: TransformerOptionsHook = super().clone()
|
| 243 |
+
c.transformers_dict = self.transformers_dict
|
| 244 |
+
c._skip_adding = self._skip_adding
|
| 245 |
+
return c
|
| 246 |
+
|
| 247 |
+
def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):
|
| 248 |
+
if not self.should_register(model, model_options, target_dict, registered):
|
| 249 |
+
return False
|
| 250 |
+
# NOTE: to_load_options will be used to manually load patches/wrappers/callbacks from hooks
|
| 251 |
+
self._skip_adding = False
|
| 252 |
+
if self.hook_scope == EnumHookScope.AllConditioning:
|
| 253 |
+
add_model_options = {"transformer_options": self.transformers_dict,
|
| 254 |
+
"to_load_options": self.transformers_dict}
|
| 255 |
+
# skip_adding if included in AllConditioning to avoid double loading
|
| 256 |
+
self._skip_adding = True
|
| 257 |
+
else:
|
| 258 |
+
add_model_options = {"to_load_options": self.transformers_dict}
|
| 259 |
+
registered.add(self)
|
| 260 |
+
comfy.patcher_extension.merge_nested_dicts(model_options, add_model_options, copy_dict1=False)
|
| 261 |
+
return True
|
| 262 |
+
|
| 263 |
+
def on_apply_hooks(self, model: ModelPatcher, transformer_options: dict[str]):
|
| 264 |
+
if not self._skip_adding:
|
| 265 |
+
comfy.patcher_extension.merge_nested_dicts(transformer_options, self.transformers_dict, copy_dict1=False)
|
| 266 |
+
|
| 267 |
+
WrapperHook = TransformerOptionsHook
|
| 268 |
+
'''Only here for backwards compatibility, WrapperHook is identical to TransformerOptionsHook.'''
|
| 269 |
+
|
| 270 |
+
class InjectionsHook(Hook):
|
| 271 |
+
def __init__(self, key: str=None, injections: list[PatcherInjection]=None,
|
| 272 |
+
hook_scope=EnumHookScope.AllConditioning):
|
| 273 |
+
super().__init__(hook_type=EnumHookType.Injections)
|
| 274 |
+
self.key = key
|
| 275 |
+
self.injections = injections
|
| 276 |
+
self.hook_scope = hook_scope
|
| 277 |
+
|
| 278 |
+
def clone(self):
|
| 279 |
+
c: InjectionsHook = super().clone()
|
| 280 |
+
c.key = self.key
|
| 281 |
+
c.injections = self.injections.copy() if self.injections else self.injections
|
| 282 |
+
return c
|
| 283 |
+
|
| 284 |
+
def add_hook_patches(self, model: ModelPatcher, model_options: dict, target_dict: dict[str], registered: HookGroup):
|
| 285 |
+
raise NotImplementedError("InjectionsHook is not supported yet in ComfyUI.")
|
| 286 |
+
|
| 287 |
+
class HookGroup:
|
| 288 |
+
'''
|
| 289 |
+
Stores groups of hooks, and allows them to be queried by type.
|
| 290 |
+
|
| 291 |
+
To prevent breaking their functionality, never modify the underlying self.hooks or self._hook_dict vars directly;
|
| 292 |
+
always use the provided functions on HookGroup.
|
| 293 |
+
'''
|
| 294 |
+
def __init__(self):
|
| 295 |
+
self.hooks: list[Hook] = []
|
| 296 |
+
self._hook_dict: dict[EnumHookType, list[Hook]] = {}
|
| 297 |
+
|
| 298 |
+
def __len__(self):
|
| 299 |
+
return len(self.hooks)
|
| 300 |
+
|
| 301 |
+
def add(self, hook: Hook):
|
| 302 |
+
if hook not in self.hooks:
|
| 303 |
+
self.hooks.append(hook)
|
| 304 |
+
self._hook_dict.setdefault(hook.hook_type, []).append(hook)
|
| 305 |
+
|
| 306 |
+
def remove(self, hook: Hook):
|
| 307 |
+
if hook in self.hooks:
|
| 308 |
+
self.hooks.remove(hook)
|
| 309 |
+
self._hook_dict[hook.hook_type].remove(hook)
|
| 310 |
+
|
| 311 |
+
def get_type(self, hook_type: EnumHookType):
|
| 312 |
+
return self._hook_dict.get(hook_type, [])
|
| 313 |
+
|
| 314 |
+
def contains(self, hook: Hook):
|
| 315 |
+
return hook in self.hooks
|
| 316 |
+
|
| 317 |
+
def is_subset_of(self, other: HookGroup):
|
| 318 |
+
self_hooks = set(self.hooks)
|
| 319 |
+
other_hooks = set(other.hooks)
|
| 320 |
+
return self_hooks.issubset(other_hooks)
|
| 321 |
+
|
| 322 |
+
def new_with_common_hooks(self, other: HookGroup):
|
| 323 |
+
c = HookGroup()
|
| 324 |
+
for hook in self.hooks:
|
| 325 |
+
if other.contains(hook):
|
| 326 |
+
c.add(hook.clone())
|
| 327 |
+
return c
|
| 328 |
+
|
| 329 |
+
def clone(self):
|
| 330 |
+
c = HookGroup()
|
| 331 |
+
for hook in self.hooks:
|
| 332 |
+
c.add(hook.clone())
|
| 333 |
+
return c
|
| 334 |
+
|
| 335 |
+
def clone_and_combine(self, other: HookGroup):
|
| 336 |
+
c = self.clone()
|
| 337 |
+
if other is not None:
|
| 338 |
+
for hook in other.hooks:
|
| 339 |
+
c.add(hook.clone())
|
| 340 |
+
return c
|
| 341 |
+
|
| 342 |
+
def set_keyframes_on_hooks(self, hook_kf: HookKeyframeGroup):
|
| 343 |
+
if hook_kf is None:
|
| 344 |
+
hook_kf = HookKeyframeGroup()
|
| 345 |
+
else:
|
| 346 |
+
hook_kf = hook_kf.clone()
|
| 347 |
+
for hook in self.hooks:
|
| 348 |
+
hook.hook_keyframe = hook_kf
|
| 349 |
+
|
| 350 |
+
def get_hooks_for_clip_schedule(self):
|
| 351 |
+
scheduled_hooks: dict[WeightHook, list[tuple[tuple[float,float], HookKeyframe]]] = {}
|
| 352 |
+
# only care about WeightHooks, for now
|
| 353 |
+
for hook in self.get_type(EnumHookType.Weight):
|
| 354 |
+
hook: WeightHook
|
| 355 |
+
hook_schedule = []
|
| 356 |
+
# if no hook keyframes, assign default value
|
| 357 |
+
if len(hook.hook_keyframe.keyframes) == 0:
|
| 358 |
+
hook_schedule.append(((0.0, 1.0), None))
|
| 359 |
+
scheduled_hooks[hook] = hook_schedule
|
| 360 |
+
continue
|
| 361 |
+
# find ranges of values
|
| 362 |
+
prev_keyframe = hook.hook_keyframe.keyframes[0]
|
| 363 |
+
for keyframe in hook.hook_keyframe.keyframes:
|
| 364 |
+
if keyframe.start_percent > prev_keyframe.start_percent and not math.isclose(keyframe.strength, prev_keyframe.strength):
|
| 365 |
+
hook_schedule.append(((prev_keyframe.start_percent, keyframe.start_percent), prev_keyframe))
|
| 366 |
+
prev_keyframe = keyframe
|
| 367 |
+
elif keyframe.start_percent == prev_keyframe.start_percent:
|
| 368 |
+
prev_keyframe = keyframe
|
| 369 |
+
# create final range, assuming last start_percent was not 1.0
|
| 370 |
+
if not math.isclose(prev_keyframe.start_percent, 1.0):
|
| 371 |
+
hook_schedule.append(((prev_keyframe.start_percent, 1.0), prev_keyframe))
|
| 372 |
+
scheduled_hooks[hook] = hook_schedule
|
| 373 |
+
# hooks should not have their schedules in a list of tuples
|
| 374 |
+
all_ranges: list[tuple[float, float]] = []
|
| 375 |
+
for range_kfs in scheduled_hooks.values():
|
| 376 |
+
for t_range, keyframe in range_kfs:
|
| 377 |
+
all_ranges.append(t_range)
|
| 378 |
+
# turn list of ranges into boundaries
|
| 379 |
+
boundaries_set = set(itertools.chain.from_iterable(all_ranges))
|
| 380 |
+
boundaries_set.add(0.0)
|
| 381 |
+
boundaries = sorted(boundaries_set)
|
| 382 |
+
real_ranges = [(boundaries[i], boundaries[i + 1]) for i in range(len(boundaries) - 1)]
|
| 383 |
+
# with real ranges defined, give appropriate hooks w/ keyframes for each range
|
| 384 |
+
scheduled_keyframes: list[tuple[tuple[float,float], list[tuple[WeightHook, HookKeyframe]]]] = []
|
| 385 |
+
for t_range in real_ranges:
|
| 386 |
+
hooks_schedule = []
|
| 387 |
+
for hook, val in scheduled_hooks.items():
|
| 388 |
+
keyframe = None
|
| 389 |
+
# check if is a keyframe that works for the current t_range
|
| 390 |
+
for stored_range, stored_kf in val:
|
| 391 |
+
# if stored start is less than current end, then fits - give it assigned keyframe
|
| 392 |
+
if stored_range[0] < t_range[1] and stored_range[1] > t_range[0]:
|
| 393 |
+
keyframe = stored_kf
|
| 394 |
+
break
|
| 395 |
+
hooks_schedule.append((hook, keyframe))
|
| 396 |
+
scheduled_keyframes.append((t_range, hooks_schedule))
|
| 397 |
+
return scheduled_keyframes
|
| 398 |
+
|
| 399 |
+
def reset(self):
|
| 400 |
+
for hook in self.hooks:
|
| 401 |
+
hook.reset()
|
| 402 |
+
|
| 403 |
+
@staticmethod
|
| 404 |
+
def combine_all_hooks(hooks_list: list[HookGroup], require_count=0) -> HookGroup:
|
| 405 |
+
actual: list[HookGroup] = []
|
| 406 |
+
for group in hooks_list:
|
| 407 |
+
if group is not None:
|
| 408 |
+
actual.append(group)
|
| 409 |
+
if len(actual) < require_count:
|
| 410 |
+
raise Exception(f"Need at least {require_count} hooks to combine, but only had {len(actual)}.")
|
| 411 |
+
# if no hooks, then return None
|
| 412 |
+
if len(actual) == 0:
|
| 413 |
+
return None
|
| 414 |
+
# if only 1 hook, just return itself without cloning
|
| 415 |
+
elif len(actual) == 1:
|
| 416 |
+
return actual[0]
|
| 417 |
+
final_hook: HookGroup = None
|
| 418 |
+
for hook in actual:
|
| 419 |
+
if final_hook is None:
|
| 420 |
+
final_hook = hook.clone()
|
| 421 |
+
else:
|
| 422 |
+
final_hook = final_hook.clone_and_combine(hook)
|
| 423 |
+
return final_hook
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
class HookKeyframe:
|
| 427 |
+
def __init__(self, strength: float, start_percent=0.0, guarantee_steps=1):
|
| 428 |
+
self.strength = strength
|
| 429 |
+
# scheduling
|
| 430 |
+
self.start_percent = float(start_percent)
|
| 431 |
+
self.start_t = 999999999.9
|
| 432 |
+
self.guarantee_steps = guarantee_steps
|
| 433 |
+
|
| 434 |
+
def get_effective_guarantee_steps(self, max_sigma: torch.Tensor):
|
| 435 |
+
'''If keyframe starts before current sampling range (max_sigma), treat as 0.'''
|
| 436 |
+
if self.start_t > max_sigma:
|
| 437 |
+
return 0
|
| 438 |
+
return self.guarantee_steps
|
| 439 |
+
|
| 440 |
+
def clone(self):
|
| 441 |
+
c = HookKeyframe(strength=self.strength,
|
| 442 |
+
start_percent=self.start_percent, guarantee_steps=self.guarantee_steps)
|
| 443 |
+
c.start_t = self.start_t
|
| 444 |
+
return c
|
| 445 |
+
|
| 446 |
+
class HookKeyframeGroup:
|
| 447 |
+
def __init__(self):
|
| 448 |
+
self.keyframes: list[HookKeyframe] = []
|
| 449 |
+
self._current_keyframe: HookKeyframe = None
|
| 450 |
+
self._current_used_steps = 0
|
| 451 |
+
self._current_index = 0
|
| 452 |
+
self._current_strength = None
|
| 453 |
+
self._curr_t = -1.
|
| 454 |
+
|
| 455 |
+
# properties shadow those of HookWeightsKeyframe
|
| 456 |
+
@property
|
| 457 |
+
def strength(self):
|
| 458 |
+
if self._current_keyframe is not None:
|
| 459 |
+
return self._current_keyframe.strength
|
| 460 |
+
return 1.0
|
| 461 |
+
|
| 462 |
+
def reset(self):
|
| 463 |
+
self._current_keyframe = None
|
| 464 |
+
self._current_used_steps = 0
|
| 465 |
+
self._current_index = 0
|
| 466 |
+
self._current_strength = None
|
| 467 |
+
self.curr_t = -1.
|
| 468 |
+
self._set_first_as_current()
|
| 469 |
+
|
| 470 |
+
def add(self, keyframe: HookKeyframe):
|
| 471 |
+
# add to end of list, then sort
|
| 472 |
+
self.keyframes.append(keyframe)
|
| 473 |
+
self.keyframes = get_sorted_list_via_attr(self.keyframes, "start_percent")
|
| 474 |
+
self._set_first_as_current()
|
| 475 |
+
|
| 476 |
+
def _set_first_as_current(self):
|
| 477 |
+
if len(self.keyframes) > 0:
|
| 478 |
+
self._current_keyframe = self.keyframes[0]
|
| 479 |
+
else:
|
| 480 |
+
self._current_keyframe = None
|
| 481 |
+
|
| 482 |
+
def has_guarantee_steps(self):
|
| 483 |
+
for kf in self.keyframes:
|
| 484 |
+
if kf.guarantee_steps > 0:
|
| 485 |
+
return True
|
| 486 |
+
return False
|
| 487 |
+
|
| 488 |
+
def has_index(self, index: int):
|
| 489 |
+
return index >= 0 and index < len(self.keyframes)
|
| 490 |
+
|
| 491 |
+
def is_empty(self):
|
| 492 |
+
return len(self.keyframes) == 0
|
| 493 |
+
|
| 494 |
+
def clone(self):
|
| 495 |
+
c = HookKeyframeGroup()
|
| 496 |
+
for keyframe in self.keyframes:
|
| 497 |
+
c.keyframes.append(keyframe.clone())
|
| 498 |
+
c._set_first_as_current()
|
| 499 |
+
return c
|
| 500 |
+
|
| 501 |
+
def initialize_timesteps(self, model: BaseModel):
|
| 502 |
+
for keyframe in self.keyframes:
|
| 503 |
+
keyframe.start_t = model.model_sampling.percent_to_sigma(keyframe.start_percent)
|
| 504 |
+
|
| 505 |
+
def prepare_current_keyframe(self, curr_t: float, transformer_options: dict[str, torch.Tensor]) -> bool:
|
| 506 |
+
if self.is_empty():
|
| 507 |
+
return False
|
| 508 |
+
if curr_t == self._curr_t:
|
| 509 |
+
return False
|
| 510 |
+
max_sigma = torch.max(transformer_options["sample_sigmas"])
|
| 511 |
+
prev_index = self._current_index
|
| 512 |
+
prev_strength = self._current_strength
|
| 513 |
+
# if met guaranteed steps, look for next keyframe in case need to switch
|
| 514 |
+
if self._current_used_steps >= self._current_keyframe.get_effective_guarantee_steps(max_sigma):
|
| 515 |
+
# if has next index, loop through and see if need to switch
|
| 516 |
+
if self.has_index(self._current_index+1):
|
| 517 |
+
for i in range(self._current_index+1, len(self.keyframes)):
|
| 518 |
+
eval_c = self.keyframes[i]
|
| 519 |
+
# check if start_t is greater or equal to curr_t
|
| 520 |
+
# NOTE: t is in terms of sigmas, not percent, so bigger number = earlier step in sampling
|
| 521 |
+
if eval_c.start_t >= curr_t:
|
| 522 |
+
self._current_index = i
|
| 523 |
+
self._current_strength = eval_c.strength
|
| 524 |
+
self._current_keyframe = eval_c
|
| 525 |
+
self._current_used_steps = 0
|
| 526 |
+
# if guarantee_steps greater than zero, stop searching for other keyframes
|
| 527 |
+
if self._current_keyframe.get_effective_guarantee_steps(max_sigma) > 0:
|
| 528 |
+
break
|
| 529 |
+
# if eval_c is outside the percent range, stop looking further
|
| 530 |
+
else:
|
| 531 |
+
break
|
| 532 |
+
# update steps current context is used
|
| 533 |
+
self._current_used_steps += 1
|
| 534 |
+
# update current timestep this was performed on
|
| 535 |
+
self._curr_t = curr_t
|
| 536 |
+
# return True if keyframe changed, False if no change
|
| 537 |
+
return prev_index != self._current_index and prev_strength != self._current_strength
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
class InterpolationMethod:
|
| 541 |
+
LINEAR = "linear"
|
| 542 |
+
EASE_IN = "ease_in"
|
| 543 |
+
EASE_OUT = "ease_out"
|
| 544 |
+
EASE_IN_OUT = "ease_in_out"
|
| 545 |
+
|
| 546 |
+
_LIST = [LINEAR, EASE_IN, EASE_OUT, EASE_IN_OUT]
|
| 547 |
+
|
| 548 |
+
@classmethod
|
| 549 |
+
def get_weights(cls, num_from: float, num_to: float, length: int, method: str, reverse=False):
|
| 550 |
+
diff = num_to - num_from
|
| 551 |
+
if method == cls.LINEAR:
|
| 552 |
+
weights = torch.linspace(num_from, num_to, length)
|
| 553 |
+
elif method == cls.EASE_IN:
|
| 554 |
+
index = torch.linspace(0, 1, length)
|
| 555 |
+
weights = diff * np.power(index, 2) + num_from
|
| 556 |
+
elif method == cls.EASE_OUT:
|
| 557 |
+
index = torch.linspace(0, 1, length)
|
| 558 |
+
weights = diff * (1 - np.power(1 - index, 2)) + num_from
|
| 559 |
+
elif method == cls.EASE_IN_OUT:
|
| 560 |
+
index = torch.linspace(0, 1, length)
|
| 561 |
+
weights = diff * ((1 - np.cos(index * np.pi)) / 2) + num_from
|
| 562 |
+
else:
|
| 563 |
+
raise ValueError(f"Unrecognized interpolation method '{method}'.")
|
| 564 |
+
if reverse:
|
| 565 |
+
weights = weights.flip(dims=(0,))
|
| 566 |
+
return weights
|
| 567 |
+
|
| 568 |
+
def get_sorted_list_via_attr(objects: list, attr: str) -> list:
|
| 569 |
+
if not objects:
|
| 570 |
+
return objects
|
| 571 |
+
elif len(objects) <= 1:
|
| 572 |
+
return [x for x in objects]
|
| 573 |
+
# now that we know we have to sort, do it following these rules:
|
| 574 |
+
# a) if objects have same value of attribute, maintain their relative order
|
| 575 |
+
# b) perform sorting of the groups of objects with same attributes
|
| 576 |
+
unique_attrs = {}
|
| 577 |
+
for o in objects:
|
| 578 |
+
val_attr = getattr(o, attr)
|
| 579 |
+
attr_list: list = unique_attrs.get(val_attr, list())
|
| 580 |
+
attr_list.append(o)
|
| 581 |
+
if val_attr not in unique_attrs:
|
| 582 |
+
unique_attrs[val_attr] = attr_list
|
| 583 |
+
# now that we have the unique attr values grouped together in relative order, sort them by key
|
| 584 |
+
sorted_attrs = dict(sorted(unique_attrs.items()))
|
| 585 |
+
# now flatten out the dict into a list to return
|
| 586 |
+
sorted_list = []
|
| 587 |
+
for object_list in sorted_attrs.values():
|
| 588 |
+
sorted_list.extend(object_list)
|
| 589 |
+
return sorted_list
|
| 590 |
+
|
| 591 |
+
def create_transformer_options_from_hooks(model: ModelPatcher, hooks: HookGroup, transformer_options: dict[str]=None):
|
| 592 |
+
# if no hooks or is not a ModelPatcher for sampling, return empty dict
|
| 593 |
+
if hooks is None or model.is_clip:
|
| 594 |
+
return {}
|
| 595 |
+
if transformer_options is None:
|
| 596 |
+
transformer_options = {}
|
| 597 |
+
for hook in hooks.get_type(EnumHookType.TransformerOptions):
|
| 598 |
+
hook: TransformerOptionsHook
|
| 599 |
+
hook.on_apply_hooks(model, transformer_options)
|
| 600 |
+
return transformer_options
|
| 601 |
+
|
| 602 |
+
def create_hook_lora(lora: dict[str, torch.Tensor], strength_model: float, strength_clip: float):
|
| 603 |
+
hook_group = HookGroup()
|
| 604 |
+
hook = WeightHook(strength_model=strength_model, strength_clip=strength_clip)
|
| 605 |
+
hook_group.add(hook)
|
| 606 |
+
hook.weights = lora
|
| 607 |
+
return hook_group
|
| 608 |
+
|
| 609 |
+
def create_hook_model_as_lora(weights_model, weights_clip, strength_model: float, strength_clip: float):
|
| 610 |
+
hook_group = HookGroup()
|
| 611 |
+
hook = WeightHook(strength_model=strength_model, strength_clip=strength_clip)
|
| 612 |
+
hook_group.add(hook)
|
| 613 |
+
patches_model = None
|
| 614 |
+
patches_clip = None
|
| 615 |
+
if weights_model is not None:
|
| 616 |
+
patches_model = {}
|
| 617 |
+
for key in weights_model:
|
| 618 |
+
patches_model[key] = ("model_as_lora", (weights_model[key],))
|
| 619 |
+
if weights_clip is not None:
|
| 620 |
+
patches_clip = {}
|
| 621 |
+
for key in weights_clip:
|
| 622 |
+
patches_clip[key] = ("model_as_lora", (weights_clip[key],))
|
| 623 |
+
hook.weights = patches_model
|
| 624 |
+
hook.weights_clip = patches_clip
|
| 625 |
+
hook.need_weight_init = False
|
| 626 |
+
return hook_group
|
| 627 |
+
|
| 628 |
+
def get_patch_weights_from_model(model: ModelPatcher, discard_model_sampling=True):
|
| 629 |
+
if model is None:
|
| 630 |
+
return None
|
| 631 |
+
patches_model: dict[str, torch.Tensor] = model.model.state_dict()
|
| 632 |
+
if discard_model_sampling:
|
| 633 |
+
# do not include ANY model_sampling components of the model that should act as a patch
|
| 634 |
+
for key in list(patches_model.keys()):
|
| 635 |
+
if key.startswith("model_sampling"):
|
| 636 |
+
patches_model.pop(key, None)
|
| 637 |
+
return patches_model
|
| 638 |
+
|
| 639 |
+
# NOTE: this function shows how to register weight hooks directly on the ModelPatchers
|
| 640 |
+
def load_hook_lora_for_models(model: ModelPatcher, clip: CLIP, lora: dict[str, torch.Tensor],
|
| 641 |
+
strength_model: float, strength_clip: float):
|
| 642 |
+
key_map = {}
|
| 643 |
+
if model is not None:
|
| 644 |
+
key_map = comfy.lora.model_lora_keys_unet(model.model, key_map)
|
| 645 |
+
if clip is not None:
|
| 646 |
+
key_map = comfy.lora.model_lora_keys_clip(clip.cond_stage_model, key_map)
|
| 647 |
+
|
| 648 |
+
hook_group = HookGroup()
|
| 649 |
+
hook = WeightHook()
|
| 650 |
+
hook_group.add(hook)
|
| 651 |
+
loaded: dict[str] = comfy.lora.load_lora(lora, key_map)
|
| 652 |
+
if model is not None:
|
| 653 |
+
new_modelpatcher = model.clone()
|
| 654 |
+
k = new_modelpatcher.add_hook_patches(hook=hook, patches=loaded, strength_patch=strength_model)
|
| 655 |
+
else:
|
| 656 |
+
k = ()
|
| 657 |
+
new_modelpatcher = None
|
| 658 |
+
|
| 659 |
+
if clip is not None:
|
| 660 |
+
new_clip = clip.clone()
|
| 661 |
+
k1 = new_clip.patcher.add_hook_patches(hook=hook, patches=loaded, strength_patch=strength_clip)
|
| 662 |
+
else:
|
| 663 |
+
k1 = ()
|
| 664 |
+
new_clip = None
|
| 665 |
+
k = set(k)
|
| 666 |
+
k1 = set(k1)
|
| 667 |
+
for x in loaded:
|
| 668 |
+
if (x not in k) and (x not in k1):
|
| 669 |
+
logging.warning(f"NOT LOADED {x}")
|
| 670 |
+
return (new_modelpatcher, new_clip, hook_group)
|
| 671 |
+
|
| 672 |
+
def _combine_hooks_from_values(c_dict: dict[str, HookGroup], values: dict[str, HookGroup], cache: dict[tuple[HookGroup, HookGroup], HookGroup]):
|
| 673 |
+
hooks_key = 'hooks'
|
| 674 |
+
# if hooks only exist in one dict, do what's needed so that it ends up in c_dict
|
| 675 |
+
if hooks_key not in values:
|
| 676 |
+
return
|
| 677 |
+
if hooks_key not in c_dict:
|
| 678 |
+
hooks_value = values.get(hooks_key, None)
|
| 679 |
+
if hooks_value is not None:
|
| 680 |
+
c_dict[hooks_key] = hooks_value
|
| 681 |
+
return
|
| 682 |
+
# otherwise, need to combine with minimum duplication via cache
|
| 683 |
+
hooks_tuple = (c_dict[hooks_key], values[hooks_key])
|
| 684 |
+
cached_hooks = cache.get(hooks_tuple, None)
|
| 685 |
+
if cached_hooks is None:
|
| 686 |
+
new_hooks = hooks_tuple[0].clone_and_combine(hooks_tuple[1])
|
| 687 |
+
cache[hooks_tuple] = new_hooks
|
| 688 |
+
c_dict[hooks_key] = new_hooks
|
| 689 |
+
else:
|
| 690 |
+
c_dict[hooks_key] = cache[hooks_tuple]
|
| 691 |
+
|
| 692 |
+
def conditioning_set_values_with_hooks(conditioning, values={}, append_hooks=True,
|
| 693 |
+
cache: dict[tuple[HookGroup, HookGroup], HookGroup]=None):
|
| 694 |
+
c = []
|
| 695 |
+
if cache is None:
|
| 696 |
+
cache = {}
|
| 697 |
+
for t in conditioning:
|
| 698 |
+
n = [t[0], t[1].copy()]
|
| 699 |
+
for k in values:
|
| 700 |
+
if append_hooks and k == 'hooks':
|
| 701 |
+
_combine_hooks_from_values(n[1], values, cache)
|
| 702 |
+
else:
|
| 703 |
+
n[1][k] = values[k]
|
| 704 |
+
c.append(n)
|
| 705 |
+
|
| 706 |
+
return c
|
| 707 |
+
|
| 708 |
+
def set_hooks_for_conditioning(cond, hooks: HookGroup, append_hooks=True, cache: dict[tuple[HookGroup, HookGroup], HookGroup]=None):
|
| 709 |
+
if hooks is None:
|
| 710 |
+
return cond
|
| 711 |
+
return conditioning_set_values_with_hooks(cond, {'hooks': hooks}, append_hooks=append_hooks, cache=cache)
|
| 712 |
+
|
| 713 |
+
def set_timesteps_for_conditioning(cond, timestep_range: tuple[float,float]):
|
| 714 |
+
if timestep_range is None:
|
| 715 |
+
return cond
|
| 716 |
+
return conditioning_set_values(cond, {"start_percent": timestep_range[0],
|
| 717 |
+
"end_percent": timestep_range[1]})
|
| 718 |
+
|
| 719 |
+
def set_mask_for_conditioning(cond, mask: torch.Tensor, set_cond_area: str, strength: float):
|
| 720 |
+
if mask is None:
|
| 721 |
+
return cond
|
| 722 |
+
set_area_to_bounds = False
|
| 723 |
+
if set_cond_area != 'default':
|
| 724 |
+
set_area_to_bounds = True
|
| 725 |
+
if len(mask.shape) < 3:
|
| 726 |
+
mask = mask.unsqueeze(0)
|
| 727 |
+
return conditioning_set_values(cond, {'mask': mask,
|
| 728 |
+
'set_area_to_bounds': set_area_to_bounds,
|
| 729 |
+
'mask_strength': strength})
|
| 730 |
+
|
| 731 |
+
def combine_conditioning(conds: list):
|
| 732 |
+
combined_conds = []
|
| 733 |
+
for cond in conds:
|
| 734 |
+
combined_conds.extend(cond)
|
| 735 |
+
return combined_conds
|
| 736 |
+
|
| 737 |
+
def combine_with_new_conds(conds: list, new_conds: list):
|
| 738 |
+
combined_conds = []
|
| 739 |
+
for c, new_c in zip(conds, new_conds):
|
| 740 |
+
combined_conds.append(combine_conditioning([c, new_c]))
|
| 741 |
+
return combined_conds
|
| 742 |
+
|
| 743 |
+
def set_conds_props(conds: list, strength: float, set_cond_area: str,
|
| 744 |
+
mask: torch.Tensor=None, hooks: HookGroup=None, timesteps_range: tuple[float,float]=None, append_hooks=True):
|
| 745 |
+
final_conds = []
|
| 746 |
+
cache = {}
|
| 747 |
+
for c in conds:
|
| 748 |
+
# first, apply lora_hook to conditioning, if provided
|
| 749 |
+
c = set_hooks_for_conditioning(c, hooks, append_hooks=append_hooks, cache=cache)
|
| 750 |
+
# next, apply mask to conditioning
|
| 751 |
+
c = set_mask_for_conditioning(cond=c, mask=mask, strength=strength, set_cond_area=set_cond_area)
|
| 752 |
+
# apply timesteps, if present
|
| 753 |
+
c = set_timesteps_for_conditioning(cond=c, timestep_range=timesteps_range)
|
| 754 |
+
# finally, apply mask to conditioning and store
|
| 755 |
+
final_conds.append(c)
|
| 756 |
+
return final_conds
|
| 757 |
+
|
| 758 |
+
def set_conds_props_and_combine(conds: list, new_conds: list, strength: float=1.0, set_cond_area: str="default",
|
| 759 |
+
mask: torch.Tensor=None, hooks: HookGroup=None, timesteps_range: tuple[float,float]=None, append_hooks=True):
|
| 760 |
+
combined_conds = []
|
| 761 |
+
cache = {}
|
| 762 |
+
for c, masked_c in zip(conds, new_conds):
|
| 763 |
+
# first, apply lora_hook to new conditioning, if provided
|
| 764 |
+
masked_c = set_hooks_for_conditioning(masked_c, hooks, append_hooks=append_hooks, cache=cache)
|
| 765 |
+
# next, apply mask to new conditioning, if provided
|
| 766 |
+
masked_c = set_mask_for_conditioning(cond=masked_c, mask=mask, set_cond_area=set_cond_area, strength=strength)
|
| 767 |
+
# apply timesteps, if present
|
| 768 |
+
masked_c = set_timesteps_for_conditioning(cond=masked_c, timestep_range=timesteps_range)
|
| 769 |
+
# finally, combine with existing conditioning and store
|
| 770 |
+
combined_conds.append(combine_conditioning([c, masked_c]))
|
| 771 |
+
return combined_conds
|
| 772 |
+
|
| 773 |
+
def set_default_conds_and_combine(conds: list, new_conds: list,
|
| 774 |
+
hooks: HookGroup=None, timesteps_range: tuple[float,float]=None, append_hooks=True):
|
| 775 |
+
combined_conds = []
|
| 776 |
+
cache = {}
|
| 777 |
+
for c, new_c in zip(conds, new_conds):
|
| 778 |
+
# first, apply lora_hook to new conditioning, if provided
|
| 779 |
+
new_c = set_hooks_for_conditioning(new_c, hooks, append_hooks=append_hooks, cache=cache)
|
| 780 |
+
# next, add default_cond key to cond so that during sampling, it can be identified
|
| 781 |
+
new_c = conditioning_set_values(new_c, {'default': True})
|
| 782 |
+
# apply timesteps, if present
|
| 783 |
+
new_c = set_timesteps_for_conditioning(cond=new_c, timestep_range=timesteps_range)
|
| 784 |
+
# finally, combine with existing conditioning and store
|
| 785 |
+
combined_conds.append(combine_conditioning([c, new_c]))
|
| 786 |
+
return combined_conds
|
comfy/image_encoders/dino2.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from comfy.text_encoders.bert import BertAttention
|
| 3 |
+
import comfy.model_management
|
| 4 |
+
from comfy.ldm.modules.attention import optimized_attention_for_device
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Dino2AttentionOutput(torch.nn.Module):
|
| 8 |
+
def __init__(self, input_dim, output_dim, layer_norm_eps, dtype, device, operations):
|
| 9 |
+
super().__init__()
|
| 10 |
+
self.dense = operations.Linear(input_dim, output_dim, dtype=dtype, device=device)
|
| 11 |
+
|
| 12 |
+
def forward(self, x):
|
| 13 |
+
return self.dense(x)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Dino2AttentionBlock(torch.nn.Module):
|
| 17 |
+
def __init__(self, embed_dim, heads, layer_norm_eps, dtype, device, operations):
|
| 18 |
+
super().__init__()
|
| 19 |
+
self.attention = BertAttention(embed_dim, heads, dtype, device, operations)
|
| 20 |
+
self.output = Dino2AttentionOutput(embed_dim, embed_dim, layer_norm_eps, dtype, device, operations)
|
| 21 |
+
|
| 22 |
+
def forward(self, x, mask, optimized_attention):
|
| 23 |
+
return self.output(self.attention(x, mask, optimized_attention))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class LayerScale(torch.nn.Module):
|
| 27 |
+
def __init__(self, dim, dtype, device, operations):
|
| 28 |
+
super().__init__()
|
| 29 |
+
self.lambda1 = torch.nn.Parameter(torch.empty(dim, device=device, dtype=dtype))
|
| 30 |
+
|
| 31 |
+
def forward(self, x):
|
| 32 |
+
return x * comfy.model_management.cast_to_device(self.lambda1, x.device, x.dtype)
|
| 33 |
+
|
| 34 |
+
class Dinov2MLP(torch.nn.Module):
|
| 35 |
+
def __init__(self, hidden_size: int, dtype, device, operations):
|
| 36 |
+
super().__init__()
|
| 37 |
+
|
| 38 |
+
mlp_ratio = 4
|
| 39 |
+
hidden_features = int(hidden_size * mlp_ratio)
|
| 40 |
+
self.fc1 = operations.Linear(hidden_size, hidden_features, bias = True, device=device, dtype=dtype)
|
| 41 |
+
self.fc2 = operations.Linear(hidden_features, hidden_size, bias = True, device=device, dtype=dtype)
|
| 42 |
+
|
| 43 |
+
def forward(self, hidden_state: torch.Tensor) -> torch.Tensor:
|
| 44 |
+
hidden_state = self.fc1(hidden_state)
|
| 45 |
+
hidden_state = torch.nn.functional.gelu(hidden_state)
|
| 46 |
+
hidden_state = self.fc2(hidden_state)
|
| 47 |
+
return hidden_state
|
| 48 |
+
|
| 49 |
+
class SwiGLUFFN(torch.nn.Module):
|
| 50 |
+
def __init__(self, dim, dtype, device, operations):
|
| 51 |
+
super().__init__()
|
| 52 |
+
in_features = out_features = dim
|
| 53 |
+
hidden_features = int(dim * 4)
|
| 54 |
+
hidden_features = (int(hidden_features * 2 / 3) + 7) // 8 * 8
|
| 55 |
+
|
| 56 |
+
self.weights_in = operations.Linear(in_features, 2 * hidden_features, bias=True, device=device, dtype=dtype)
|
| 57 |
+
self.weights_out = operations.Linear(hidden_features, out_features, bias=True, device=device, dtype=dtype)
|
| 58 |
+
|
| 59 |
+
def forward(self, x):
|
| 60 |
+
x = self.weights_in(x)
|
| 61 |
+
x1, x2 = x.chunk(2, dim=-1)
|
| 62 |
+
x = torch.nn.functional.silu(x1) * x2
|
| 63 |
+
return self.weights_out(x)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class Dino2Block(torch.nn.Module):
|
| 67 |
+
def __init__(self, dim, num_heads, layer_norm_eps, dtype, device, operations, use_swiglu_ffn):
|
| 68 |
+
super().__init__()
|
| 69 |
+
self.attention = Dino2AttentionBlock(dim, num_heads, layer_norm_eps, dtype, device, operations)
|
| 70 |
+
self.layer_scale1 = LayerScale(dim, dtype, device, operations)
|
| 71 |
+
self.layer_scale2 = LayerScale(dim, dtype, device, operations)
|
| 72 |
+
if use_swiglu_ffn:
|
| 73 |
+
self.mlp = SwiGLUFFN(dim, dtype, device, operations)
|
| 74 |
+
else:
|
| 75 |
+
self.mlp = Dinov2MLP(dim, dtype, device, operations)
|
| 76 |
+
self.norm1 = operations.LayerNorm(dim, eps=layer_norm_eps, dtype=dtype, device=device)
|
| 77 |
+
self.norm2 = operations.LayerNorm(dim, eps=layer_norm_eps, dtype=dtype, device=device)
|
| 78 |
+
|
| 79 |
+
def forward(self, x, optimized_attention):
|
| 80 |
+
x = x + self.layer_scale1(self.attention(self.norm1(x), None, optimized_attention))
|
| 81 |
+
x = x + self.layer_scale2(self.mlp(self.norm2(x)))
|
| 82 |
+
return x
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class Dino2Encoder(torch.nn.Module):
|
| 86 |
+
def __init__(self, dim, num_heads, layer_norm_eps, num_layers, dtype, device, operations, use_swiglu_ffn):
|
| 87 |
+
super().__init__()
|
| 88 |
+
self.layer = torch.nn.ModuleList([Dino2Block(dim, num_heads, layer_norm_eps, dtype, device, operations, use_swiglu_ffn = use_swiglu_ffn)
|
| 89 |
+
for _ in range(num_layers)])
|
| 90 |
+
|
| 91 |
+
def forward(self, x, intermediate_output=None):
|
| 92 |
+
optimized_attention = optimized_attention_for_device(x.device, False, small_input=True)
|
| 93 |
+
|
| 94 |
+
if intermediate_output is not None:
|
| 95 |
+
if intermediate_output < 0:
|
| 96 |
+
intermediate_output = len(self.layer) + intermediate_output
|
| 97 |
+
|
| 98 |
+
intermediate = None
|
| 99 |
+
for i, layer in enumerate(self.layer):
|
| 100 |
+
x = layer(x, optimized_attention)
|
| 101 |
+
if i == intermediate_output:
|
| 102 |
+
intermediate = x.clone()
|
| 103 |
+
return x, intermediate
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class Dino2PatchEmbeddings(torch.nn.Module):
|
| 107 |
+
def __init__(self, dim, num_channels=3, patch_size=14, image_size=518, dtype=None, device=None, operations=None):
|
| 108 |
+
super().__init__()
|
| 109 |
+
self.patch_size = patch_size
|
| 110 |
+
self.projection = operations.Conv2d(
|
| 111 |
+
in_channels=num_channels,
|
| 112 |
+
out_channels=dim,
|
| 113 |
+
kernel_size=patch_size,
|
| 114 |
+
stride=patch_size,
|
| 115 |
+
bias=True,
|
| 116 |
+
dtype=dtype,
|
| 117 |
+
device=device
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
def forward(self, pixel_values):
|
| 121 |
+
return self.projection(pixel_values).flatten(2).transpose(1, 2)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class Dino2Embeddings(torch.nn.Module):
|
| 125 |
+
def __init__(self, dim, dtype, device, operations):
|
| 126 |
+
super().__init__()
|
| 127 |
+
patch_size = 14
|
| 128 |
+
image_size = 518
|
| 129 |
+
self.patch_size = patch_size
|
| 130 |
+
|
| 131 |
+
self.patch_embeddings = Dino2PatchEmbeddings(dim, patch_size=patch_size, image_size=image_size, dtype=dtype, device=device, operations=operations)
|
| 132 |
+
self.position_embeddings = torch.nn.Parameter(torch.empty(1, (image_size // patch_size) ** 2 + 1, dim, dtype=dtype, device=device))
|
| 133 |
+
self.cls_token = torch.nn.Parameter(torch.empty(1, 1, dim, dtype=dtype, device=device)) # mask_token is a pre-training param, kept only so strict loading accepts the key.
|
| 134 |
+
self.mask_token = torch.nn.Parameter(torch.empty(1, dim, dtype=dtype, device=device))
|
| 135 |
+
|
| 136 |
+
def interpolate_pos_encoding(self, x, h_pixels, w_pixels):
|
| 137 |
+
pos_embed = comfy.model_management.cast_to_device(self.position_embeddings, x.device, torch.float32)
|
| 138 |
+
|
| 139 |
+
class_pos = pos_embed[:, 0:1]
|
| 140 |
+
patch_pos = pos_embed[:, 1:]
|
| 141 |
+
N = patch_pos.shape[1]
|
| 142 |
+
M = int(N ** 0.5)
|
| 143 |
+
h0 = h_pixels // self.patch_size
|
| 144 |
+
w0 = w_pixels // self.patch_size
|
| 145 |
+
scale_factor = ((h0 + 0.1) / M, (w0 + 0.1) / M) # +0.1 matches upstream DINOv2's FP-rounding workaround so the interpolate output size lands on (h0, w0).
|
| 146 |
+
|
| 147 |
+
patch_pos = patch_pos.reshape(1, M, M, -1).permute(0, 3, 1, 2)
|
| 148 |
+
patch_pos = torch.nn.functional.interpolate(patch_pos, scale_factor=scale_factor, mode="bicubic", antialias=False)
|
| 149 |
+
patch_pos = patch_pos.permute(0, 2, 3, 1).flatten(1, 2)
|
| 150 |
+
return torch.cat((class_pos, patch_pos), dim=1).to(x.dtype)
|
| 151 |
+
|
| 152 |
+
def forward(self, pixel_values):
|
| 153 |
+
x = self.patch_embeddings(pixel_values)
|
| 154 |
+
x = torch.cat((self.cls_token.to(device=x.device, dtype=x.dtype).expand(x.shape[0], -1, -1), x), dim=1)
|
| 155 |
+
if x.shape[1] - 1 == self.position_embeddings.shape[1] - 1:
|
| 156 |
+
x = x + comfy.model_management.cast_to_device(self.position_embeddings, x.device, x.dtype)
|
| 157 |
+
else:
|
| 158 |
+
h, w = pixel_values.shape[-2:]
|
| 159 |
+
x = x + self.interpolate_pos_encoding(x, h, w)
|
| 160 |
+
return x
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class Dinov2Model(torch.nn.Module):
|
| 164 |
+
def __init__(self, config_dict, dtype, device, operations):
|
| 165 |
+
super().__init__()
|
| 166 |
+
num_layers = config_dict["num_hidden_layers"]
|
| 167 |
+
dim = config_dict["hidden_size"]
|
| 168 |
+
heads = config_dict["num_attention_heads"]
|
| 169 |
+
layer_norm_eps = config_dict["layer_norm_eps"]
|
| 170 |
+
use_swiglu_ffn = config_dict["use_swiglu_ffn"]
|
| 171 |
+
|
| 172 |
+
self.embeddings = Dino2Embeddings(dim, dtype, device, operations)
|
| 173 |
+
self.encoder = Dino2Encoder(dim, heads, layer_norm_eps, num_layers, dtype, device, operations, use_swiglu_ffn = use_swiglu_ffn)
|
| 174 |
+
self.layernorm = operations.LayerNorm(dim, eps=layer_norm_eps, dtype=dtype, device=device)
|
| 175 |
+
|
| 176 |
+
def forward(self, pixel_values, attention_mask=None, intermediate_output=None):
|
| 177 |
+
x = self.embeddings(pixel_values)
|
| 178 |
+
x, i = self.encoder(x, intermediate_output=intermediate_output)
|
| 179 |
+
x = self.layernorm(x)
|
| 180 |
+
pooled_output = x[:, 0, :]
|
| 181 |
+
return x, i, pooled_output, None
|
| 182 |
+
|
| 183 |
+
def get_intermediate_layers(self, pixel_values, indices, apply_norm=True):
|
| 184 |
+
x = self.embeddings(pixel_values)
|
| 185 |
+
optimized_attention = optimized_attention_for_device(x.device, False, small_input=True)
|
| 186 |
+
n_layers = len(self.encoder.layer)
|
| 187 |
+
resolved = [(i if i >= 0 else n_layers + i) for i in indices]
|
| 188 |
+
target = set(resolved)
|
| 189 |
+
max_idx = max(resolved)
|
| 190 |
+
n_skip = 1 # skip cls token
|
| 191 |
+
cache = {}
|
| 192 |
+
for i, layer in enumerate(self.encoder.layer):
|
| 193 |
+
x = layer(x, optimized_attention)
|
| 194 |
+
if i in target:
|
| 195 |
+
normed = self.layernorm(x) if apply_norm else x
|
| 196 |
+
cache[i] = (normed[:, n_skip:], normed[:, 0])
|
| 197 |
+
if i >= max_idx:
|
| 198 |
+
break
|
| 199 |
+
return [cache[i] for i in resolved]
|