saik0s commited on
Commit
47223ae
·
verified ·
1 Parent(s): 232ab33

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/ISSUE_TEMPLATE/bug-report.yml +58 -0
  2. .github/ISSUE_TEMPLATE/config.yml +11 -0
  3. .github/ISSUE_TEMPLATE/feature-request.yml +32 -0
  4. .github/ISSUE_TEMPLATE/user-support.yml +40 -0
  5. .github/PULL_REQUEST_TEMPLATE/api-node.md +21 -0
  6. .github/scripts/check-ai-co-authors.sh +103 -0
  7. .github/workflows/api-node-template.yml +58 -0
  8. .github/workflows/backport_release.yaml +519 -0
  9. .github/workflows/check-ai-co-authors.yml +19 -0
  10. .github/workflows/check-line-endings.yml +40 -0
  11. .github/workflows/detect-unreviewed-merge.yml +24 -0
  12. .github/workflows/openapi-lint.yml +31 -0
  13. .github/workflows/pullrequest-ci-run.yml +53 -0
  14. .github/workflows/release-stable-all.yml +78 -0
  15. .github/workflows/release-webhook.yml +144 -0
  16. .github/workflows/ruff.yml +48 -0
  17. .github/workflows/stable-release.yml +172 -0
  18. .github/workflows/stale-issues.yml +21 -0
  19. .github/workflows/tag-dispatch-cloud.yml +45 -0
  20. .github/workflows/test-build.yml +31 -0
  21. .github/workflows/test-ci.yml +99 -0
  22. .github/workflows/test-execution.yml +30 -0
  23. .github/workflows/test-launch.yml +47 -0
  24. .github/workflows/test-unit.yml +30 -0
  25. .github/workflows/update-api-stubs.yml +56 -0
  26. .github/workflows/update-ci-container.yml +59 -0
  27. .github/workflows/update-version.yml +59 -0
  28. .github/workflows/windows_release_dependencies.yml +72 -0
  29. .github/workflows/windows_release_dependencies_manual.yml +64 -0
  30. .github/workflows/windows_release_nightly_pytorch.yml +93 -0
  31. .github/workflows/windows_release_package.yml +106 -0
  32. comfy/audio_encoders/audio_encoders.py +92 -0
  33. comfy/audio_encoders/wav2vec2.py +252 -0
  34. comfy/audio_encoders/whisper.py +186 -0
  35. comfy/background_removal/birefnet.json +7 -0
  36. comfy/background_removal/birefnet.py +689 -0
  37. comfy/cldm/cldm.py +434 -0
  38. comfy/cldm/control_types.py +10 -0
  39. comfy/cldm/dit_embedder.py +120 -0
  40. comfy/cldm/mmdit.py +81 -0
  41. comfy/comfy_types/README.md +43 -0
  42. comfy/comfy_types/__init__.py +46 -0
  43. comfy/comfy_types/examples/example_nodes.py +28 -0
  44. comfy/comfy_types/node_typing.py +353 -0
  45. comfy/diffusers_load.py +36 -0
  46. comfy/extra_samplers/uni_pc.py +873 -0
  47. comfy/float.py +266 -0
  48. comfy/gligen.py +299 -0
  49. comfy/hooks.py +786 -0
  50. 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
+ ![INPUT_TYPES auto-completion in Visual Studio Code](examples/input_types.png)
36
+
37
+ ### `INPUT_TYPES` return dict
38
+
39
+ ![INPUT_TYPES return value type hinting in Visual Studio Code](examples/required_hint.png)
40
+
41
+ ### Options for individual inputs
42
+
43
+ ![INPUT_TYPES return value option auto-completion in Visual Studio Code](examples/input_options.png)
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]