Instructions to use AlperKTS/Krea-2-SVDQuant-ComfyUI with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusion Single File
How to use AlperKTS/Krea-2-SVDQuant-ComfyUI with Diffusion Single File:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Sync the bundled nodes, workflows and docs with GitHub master (099a051)
Browse filesThe mirrored copy had drifted to roughly the 5f40849 era, before the all-in-one work existed. That left the all-in-one checkpoint in this repo unloadable by the code shipped next to it -- Krea2SVDQuantCheckpointLoader was not in it -- and kept the dynamic-VRAM pin that made iteration time jump to 30-100 s past ~12 GB.
Updates 12 files and adds 15, including build_all_in_one.py and the 12 workflow JSONs that were missing.
- BENCHMARKS.md +10 -1
- README.md +131 -108
- ROADMAP.md +157 -33
- TROUBLESHOOTING.md +94 -5
- __init__.py +27 -27
- build_all_in_one.py +435 -0
- quantize_krea2.py +923 -844
- svdquant_capture.py +27 -5
- svdquant_lora.py +76 -13
- svdquant_quantize.py +310 -6
- svdquant_w4a4.py +230 -43
- tools/bake_adapter.py +10 -5
- tools/build_all_in_one.py +100 -0
- tools/build_workflows.py +561 -5
- workflows/README.md +209 -0
- workflows/krea2_quantize.json +126 -0
- workflows/krea2_quantize_all_in_one.json +128 -0
- workflows/krea2_quantize_all_in_one_api.json +30 -0
- workflows/krea2_quantize_api.json +28 -0
- workflows/krea2_quantize_calibrated.json +557 -0
- workflows/krea2_quantize_calibrated_api.json +139 -0
- workflows/krea2_svdquant_diagnostics.json +323 -0
- workflows/krea2_svdquant_diagnostics_api.json +68 -0
- workflows/krea2_turbo_all_in_one_t2i.json +517 -0
- workflows/krea2_turbo_all_in_one_t2i_api.json +106 -0
- workflows/krea2_turbo_svdquant_w4a4_lora.json +631 -0
- workflows/krea2_turbo_svdquant_w4a4_lora_api.json +141 -0
BENCHMARKS.md
CHANGED
|
@@ -4,6 +4,15 @@ Two independent tests on **Krea 2 Turbo**, all checkpoints produced by `quantize
|
|
| 4 |
in this repo. Same seed across every checkpoint in a given test so images are directly
|
| 5 |
comparable. RTX 3090, cu130 torch build.
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
> **The quality scores in Tests 1 and 2 have been withdrawn (2026-07-27).** They came from
|
| 8 |
> an LLM judge (Gemini 3.5 Flash Lite) that turned out to be saturated: 9 of the 12 rows in
|
| 9 |
> the LoRA table scored a flat 10.00/10, and the whole t2i table spanned 0.46 points. An
|
|
@@ -267,7 +276,7 @@ configuration in which the plain objective is the better choice.
|
|
| 267 |
|
| 268 |
```bash
|
| 269 |
python quantize_krea2.py turbo.safetensors --format svdq --rank 256 \
|
| 270 |
-
--act-stats
|
| 271 |
```
|
| 272 |
|
| 273 |
Ruled out as confounds: `r256` and `r256-actaware` share source, rank, `refine_iters=100`,
|
|
|
|
| 4 |
in this repo. Same seed across every checkpoint in a given test so images are directly
|
| 5 |
comparable. RTX 3090, cu130 torch build.
|
| 6 |
|
| 7 |
+
> **Which patcher these were measured on.** Every speed number here was taken while the
|
| 8 |
+
> svdq loader still pinned the model to the classic `ModelPatcher` (`disable_dynamic=True`).
|
| 9 |
+
> That pin is gone; the default is now ComfyUI's dynamic-VRAM patcher. On a card where the
|
| 10 |
+
> model fits, the patcher is not on the hot path and the numbers should be unaffected — a
|
| 11 |
+
> spot check at rank 256, 1024², under the dynamic patcher measured 0.794 s/step, which sits
|
| 12 |
+
> in the same range as the table below. But that is one point, not a re-run: **these tables
|
| 13 |
+
> have not been regenerated since the change.** `tools/speed_bench.py` produces them if you
|
| 14 |
+
> want them re-measured.
|
| 15 |
+
|
| 16 |
> **The quality scores in Tests 1 and 2 have been withdrawn (2026-07-27).** They came from
|
| 17 |
> an LLM judge (Gemini 3.5 Flash Lite) that turned out to be saturated: 9 of the 12 rows in
|
| 18 |
> the LoRA table scored a flat 10.00/10, and the whole t2i table spanned 0.46 points. An
|
|
|
|
| 276 |
|
| 277 |
```bash
|
| 278 |
python quantize_krea2.py turbo.safetensors --format svdq --rank 256 \
|
| 279 |
+
--act-stats krea2_act_stats_turbo.safetensors
|
| 280 |
```
|
| 281 |
|
| 282 |
Ruled out as confounds: `r256` and `r256-actaware` share source, rank, `refine_iters=100`,
|
README.md
CHANGED
|
@@ -1,122 +1,89 @@
|
|
| 1 |
-
|
| 2 |
-
license: other
|
| 3 |
-
license_name: krea-2-community-license
|
| 4 |
-
license_link: https://www.krea.ai/krea-2-licensing
|
| 5 |
-
library_name: diffusers
|
| 6 |
-
tags:
|
| 7 |
-
- image-generation
|
| 8 |
-
- comfyui
|
| 9 |
-
- quantization
|
| 10 |
-
- int8
|
| 11 |
-
- int4
|
| 12 |
-
- svdquant
|
| 13 |
-
- krea2
|
| 14 |
-
- krea
|
| 15 |
-
- diffusion
|
| 16 |
-
- transformer
|
| 17 |
-
- lowvram
|
| 18 |
-
base_model: krea/krea-2
|
| 19 |
-
pipeline_tag: text-to-image
|
| 20 |
-
---
|
| 21 |
|
| 22 |
-
|
|
|
|
| 23 |
|
| 24 |
Quantized **Krea 2** checkpoints for ComfyUI — about **2x faster** and **a third the size**
|
| 25 |
of the usual FP8 version, with no calibration dataset needed, on both **Krea 2 Turbo**
|
| 26 |
-
(distilled, 8 steps) and the **base** release. This repo holds the custom nodes
|
| 27 |
-
`quantize_krea2.py` conversion script; the `.safetensors` files
|
| 28 |
Hugging Face.
|
| 29 |
|
| 30 |
Works on **any modern NVIDIA GPU** — INT8/W4A4 tensor cores go back to Turing (RTX 20-series).
|
| 31 |
-
Benchmarked on an RTX 3090 (Ampere)
|
| 32 |
-
|
| 33 |
-
scratch against ComfyUI's own quantization backend and is reproducible: `quantize_krea2.py`
|
| 34 |
-
regenerates any checkpoint from a BF16 source in 40-100 seconds, or ~6 minutes with the
|
| 35 |
-
default low-rank refinement.
|
| 36 |
|
| 37 |
> **Requires a cu130 (CUDA 13) or newer PyTorch build.** ComfyUI disables `comfy_kitchen`'s
|
| 38 |
> CUDA backend on older torch builds, which silently drops every quantized checkpoint onto a
|
| 39 |
> pure-Python fallback that is *slower than bf16*. If these checkpoints are slower than FP8
|
| 40 |
-
> for you,
|
| 41 |
-
> [TROUBLESHOOTING.md](TROUBLESHOOTING.md#its-slower-than-fp8--slower-than-bf16).
|
| 42 |
-
|
| 43 |
-
This is a community-produced modification of Krea 2, not an official Krea product — license
|
| 44 |
-
and attribution are at the [bottom of this page](#attribution). Read them before using these
|
| 45 |
-
weights, in particular the revenue threshold on commercial use.
|
| 46 |
|
| 47 |
-
|
| 48 |
-
> builds in `checkpoints/legacy/`. Direct links to the old repository-root paths will 404 --
|
| 49 |
-
> the root had ten loose 8 GB files with no indication which one to take.
|
| 50 |
|
| 51 |
-
##
|
| 52 |
|
| 53 |
-
|
|
|
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
was just fitted against measured per-channel activation energy instead of assuming it is
|
| 66 |
-
uniform ([`--act-stats`](#activation-aware-branch---act-stats)). Without a LoRA that moves
|
| 67 |
-
LPIPS-to-BF16 from 0.3378 to **0.2825** (t=4.68 over 32 paired cells); with a LoRA it measures
|
| 68 |
-
no worse. There is no case where the plain build is the better pick. The calibration file it
|
| 69 |
-
was built from is in `calibration/`, so the build is reproducible.
|
| 70 |
-
|
| 71 |
-
The base file is the same recipe applied to `raw.safetensors`, built from its own calibration
|
| 72 |
-
pass (`calibration/krea2_act_stats_base.safetensors`, 8 prompts on the BF16 base model at its
|
| 73 |
-
own sampler settings). **It has not been through the paired benchmark yet** — the act-aware
|
| 74 |
-
numbers below are turbo measurements and are not evidence about the base build. Treat it as
|
| 75 |
-
"the base equivalent of the recommended turbo file", not as a measured improvement over
|
| 76 |
-
`Krea2-Base-SVDQuant-W4A4-rank256`.
|
| 77 |
-
|
| 78 |
-
**Rank only matters if you load LoRAs.** Without one, ranks 64 / 128 / 256 are statistically
|
| 79 |
-
indistinguishable. With one, rank 256 wins clearly and rank 64 loses most of its advantage
|
| 80 |
-
over having no branch at all
|
| 81 |
-
([Test 3](BENCHMARKS.md#test-3--paired-lpips-fidelity-with-and-without-a-lora)).
|
| 82 |
-
|
| 83 |
-
Every file records how it was built in its safetensors metadata, so you can check what you
|
| 84 |
-
downloaded rather than trusting this table:
|
| 85 |
-
|
| 86 |
-
```python
|
| 87 |
-
from safetensors import safe_open
|
| 88 |
-
with safe_open("Krea2-Turbo-SVDQuant-W4A4-rank256-actaware.safetensors", framework="pt") as f:
|
| 89 |
-
print(f.metadata())
|
| 90 |
-
```
|
| 91 |
|
| 92 |
-
|
| 93 |
-
> before 2026-07-26) was built without refinement and carries no metadata at all. If yours
|
| 94 |
-
> returns `None`, re-download — the whole rank ladder is flat without refinement.
|
| 95 |
|
| 96 |
-
##
|
|
|
|
| 97 |
|
| 98 |
-
1. **Install
|
| 99 |
```bash
|
| 100 |
-
git clone https://github.com/alperktt/Krea-2-SVDQuant-ComfyUI custom_nodes/
|
| 101 |
```
|
| 102 |
-
|
| 103 |
-
ComfyUI.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
## See it
|
| 122 |
|
|
@@ -200,13 +167,15 @@ a calibration pass if you have one, but the default path needs nothing.
|
|
| 200 |
|
| 201 |
## The nodes
|
| 202 |
|
| 203 |
-
Installing this adds
|
| 204 |
|
| 205 |
| node | what it is for |
|
| 206 |
|---|---|
|
| 207 |
-
| **Krea2 SVDQuant W4A4 Loader** | Loads
|
| 208 |
-
| **Krea2 SVDQuant
|
| 209 |
-
| **Krea2 SVDQuant
|
|
|
|
|
|
|
| 210 |
| **Krea2 SVDQuant Diagnostics** | Backend dispatch, memory accounting, per-layer timings, profiler table |
|
| 211 |
| **Krea2 SVDQuant Env Check** | Is the int4 kernel available at all? Needs no model, so you can ask before downloading 8 GB |
|
| 212 |
| **Krea2 SVDQuant Capture Start** / **Capture Save** | Record activation statistics for an activation-aware build |
|
|
@@ -217,12 +186,13 @@ Installing this adds seven nodes, all under **Krea2/SVDQuant**:
|
|
| 217 |
| file | what it is |
|
| 218 |
|---|---|
|
| 219 |
| `quantize_krea2.py` | Converts a BF16 Krea 2 checkpoint to int8, w4a4, or w4a4 + low-rank (svdq). Also the CLI |
|
| 220 |
-
| `svdquant_w4a4.py` | The **W4A4 Loader**
|
| 221 |
| `svdquant_lora.py` | The **LoRA Loader** node — applies LoRAs as a parallel branch so the 4-bit weight is never dequantized |
|
| 222 |
| `svdquant_quantize.py` | The **Quantize** node — the quantizer above, from inside ComfyUI |
|
| 223 |
| `svdquant_capture.py` | The **Capture Start/Save** nodes — record per-channel activation RMS for `--act-stats` |
|
| 224 |
| `svdquant_diag.py` | The **Diagnostics** and **Env Check** nodes |
|
| 225 |
| `diagnose.py` | The same reports from a terminal, without starting ComfyUI (`--mode all` for everything) |
|
|
|
|
| 226 |
| `tools/fidelity_bench.py` | Paired multi-seed multi-LoRA LPIPS harness. Every fidelity claim in this repo comes from it |
|
| 227 |
| `tools/speed_bench.py` | Per-step speed, by timing each checkpoint at two step counts and taking the slope. Every s/step number in this repo comes from it |
|
| 228 |
| `tools/contact_sheet.py` | Builds the contact sheets in [GALLERY.md](GALLERY.md) — for looking at, not scoring |
|
|
@@ -237,12 +207,37 @@ is **API format** (POST to `/prompt`, no layout). Both come out of `build_workfl
|
|
| 237 |
|
| 238 |
## Build your own checkpoint
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
From a terminal:
|
| 241 |
|
| 242 |
```bash
|
| 243 |
-
cd ComfyUI/custom_nodes/
|
| 244 |
python quantize_krea2.py /path/to/krea2_bf16.safetensors --format svdq --rank 256 \
|
| 245 |
-
--act-stats
|
| 246 |
```
|
| 247 |
|
| 248 |
`--format int8` and `--format w4a4` also work (`int8` is the most faithful option and still
|
|
@@ -251,9 +246,16 @@ python quantize_krea2.py /path/to/krea2_bf16.safetensors --format svdq --rank 25
|
|
| 251 |
the metadata — it does not change the quantization, since layer selection keys off block
|
| 252 |
naming that Turbo and base share.
|
| 253 |
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
**`rank` and `refine_iters` are one lever, not two.** With refinement off a rank sweep is flat
|
| 259 |
— rank 16 and rank 256 land within noise of each other — so raising rank without
|
|
@@ -336,6 +338,25 @@ one that worked, and it optimises against something else.
|
|
| 336 |
|
| 337 |
</details>
|
| 338 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
## What was measured
|
| 340 |
|
| 341 |
Full tables, methodology and caveats: **[BENCHMARKS.md](BENCHMARKS.md)**. Images:
|
|
@@ -453,10 +474,12 @@ coverage.
|
|
| 453 |
|
| 454 |
## Troubleshooting
|
| 455 |
|
| 456 |
-
**[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** covers the
|
| 457 |
generation slower than FP8 (nearly always a pre-cu130 torch build), no speedup at all on
|
| 458 |
-
RTX 20-series, `Pin error.` in the console,
|
| 459 |
-
|
|
|
|
|
|
|
| 460 |
|
| 461 |
Start with the **Krea2 SVDQuant Diagnostics** node between the loader and the KSampler, or:
|
| 462 |
|
|
|
|
| 1 |
+
# Krea 2 SVDQuant & Native Quantization for ComfyUI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
**Weights and all example images:**
|
| 4 |
+
[huggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI](https://huggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI)
|
| 5 |
|
| 6 |
Quantized **Krea 2** checkpoints for ComfyUI — about **2x faster** and **a third the size**
|
| 7 |
of the usual FP8 version, with no calibration dataset needed, on both **Krea 2 Turbo**
|
| 8 |
+
(distilled, 8 steps) and the **base** release. This repo holds the custom nodes, the All-in-One
|
| 9 |
+
builder, and the `quantize_krea2.py` conversion script; the `.safetensors` files live on
|
| 10 |
Hugging Face.
|
| 11 |
|
| 12 |
Works on **any modern NVIDIA GPU** — INT8/W4A4 tensor cores go back to Turing (RTX 20-series).
|
| 13 |
+
Benchmarked on an RTX 3090 (Ampere). Everything here was built from scratch against ComfyUI's
|
| 14 |
+
own quantization backend and is 100% reproducible.
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
> **Requires a cu130 (CUDA 13) or newer PyTorch build.** ComfyUI disables `comfy_kitchen`'s
|
| 17 |
> CUDA backend on older torch builds, which silently drops every quantized checkpoint onto a
|
| 18 |
> pure-Python fallback that is *slower than bf16*. If these checkpoints are slower than FP8
|
| 19 |
+
> for you, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md#its-slower-than-fp8--slower-than-bf16).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
---
|
|
|
|
|
|
|
| 22 |
|
| 23 |
+
## 🚀 Quick Start (Choose Your Setup)
|
| 24 |
|
| 25 |
+
### Option 1: 🌟 1-Click All-in-One Checkpoint (Recommended)
|
| 26 |
+
> **Zero extra downloads!** DiT model, 4-bit Qwen3-VL 4B text encoder, and VAE are combined into **a single 12.77 GB file**.
|
| 27 |
|
| 28 |
+
1. **Install custom nodes:**
|
| 29 |
+
```bash
|
| 30 |
+
git clone https://github.com/alperktt/Krea-2-SVDQuant-ComfyUI custom_nodes/krea-2-svdquant
|
| 31 |
+
```
|
| 32 |
+
2. **Download the unified checkpoint:**
|
| 33 |
+
- Download `Krea2-Turbo-AllInOne-SVDQuant-W4A4-rank256-actaware-TEW4A4.safetensors` (12.77 GB) from [Hugging Face](https://huggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI).
|
| 34 |
+
- Place it in: `ComfyUI/models/checkpoints/`
|
| 35 |
+
3. **Run in ComfyUI:**
|
| 36 |
+
- Drag [`workflows/krea2_turbo_all_in_one_t2i.json`](workflows/krea2_turbo_all_in_one_t2i.json) into your ComfyUI canvas.
|
| 37 |
+
- It uses the **Krea2 SVDQuant Checkpoint Loader** node to load everything in 1 click. Press **Queue Prompt**!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
---
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
### Option 2: 📦 Modular 3-File Setup
|
| 42 |
+
> If you prefer downloading the DiT, Text Encoder, and VAE separately:
|
| 43 |
|
| 44 |
+
1. **Install custom nodes:**
|
| 45 |
```bash
|
| 46 |
+
git clone https://github.com/alperktt/Krea-2-SVDQuant-ComfyUI custom_nodes/krea-2-svdquant
|
| 47 |
```
|
| 48 |
+
2. **Download the 3 files:**
|
| 49 |
+
- **DiT Checkpoint** → `ComfyUI/models/diffusion_models/` (from [Hugging Face](https://huggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI))
|
| 50 |
+
- **Text Encoder** → [`qwen3vl_4b_fp8_scaled.safetensors`](https://huggingface.co/Comfy-Org/Krea-2/resolve/main/text_encoders/qwen3vl_4b_fp8_scaled.safetensors) → `ComfyUI/models/text_encoders/`
|
| 51 |
+
- **VAE** → [`qwen_image_vae.safetensors`](https://huggingface.co/Comfy-Org/Krea-2/resolve/main/vae/qwen_image_vae.safetensors) → `ComfyUI/models/vae/`
|
| 52 |
+
3. **Run in ComfyUI:**
|
| 53 |
+
- Drag [`workflows/krea2_turbo_svdquant_w4a4_t2i.json`](workflows/krea2_turbo_svdquant_w4a4_t2i.json) into canvas.
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## 📥 Which Checkpoint to Download
|
| 58 |
|
| 59 |
+
| Checkpoint File | Format & Type | Size | Destination Folder | Loader Node | Recommended For |
|
| 60 |
+
|---|---|---|---|---|---|
|
| 61 |
+
| **`Krea2-Turbo-AllInOne-SVDQuant-W4A4-rank256-actaware-TEW4A4.safetensors`** | **All-in-One** (DiT + 4-bit TE + VAE, rank 256 act-aware) | **12.77 GB** | `models/checkpoints/` | **Krea2 SVDQuant Checkpoint Loader** | **Best for 1-click generation. Easiest setup, and the rank that holds up under a LoRA.** |
|
| 62 |
+
| **`Krea2-Turbo-SVDQuant-W4A4-rank256-actaware.safetensors`** | Standalone DiT (svdq r256 actaware) | 9.10 GB | `models/diffusion_models/` | **Krea2 SVDQuant W4A4 Loader** | Best standalone DiT with LoRA support |
|
| 63 |
+
| **`Krea2-Turbo-SVDQuant-W4A4-rank64.safetensors`** | Standalone DiT (svdq r64) | 7.90 GB | `models/diffusion_models/` | **Krea2 SVDQuant W4A4 Loader** | Smaller footprint without LoRA |
|
| 64 |
+
| **`Krea2-Turbo-W4A4-noLowRank.safetensors`** | Standalone DiT (branchless w4a4) | 7.50 GB | `models/diffusion_models/` | Stock **UNETLoader** | Fastest (~9% faster per step) |
|
| 65 |
+
| **`Krea2-Base-SVDQuant-W4A4-rank256-actaware.safetensors`** | Standalone DiT (Base release) | 9.10 GB | `models/diffusion_models/` | **Krea2 SVDQuant W4A4 Loader** | Base (50 steps, cfg 3.5, real negative) |
|
| 66 |
|
| 67 |
+
> 💡 **Folder Tip:**
|
| 68 |
+
> - **All-in-One files** go into `ComfyUI/models/checkpoints/`.
|
| 69 |
+
> - **Standalone DiT files** go into `ComfyUI/models/diffusion_models/`.
|
| 70 |
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## 📑 Workflows Overview
|
| 74 |
+
|
| 75 |
+
Eight ready-to-use workflows in [`workflows/`](workflows/):
|
| 76 |
|
| 77 |
+
| Workflow File | Purpose | Setup Type |
|
| 78 |
+
|---|---|---|
|
| 79 |
+
| [`krea2_turbo_all_in_one_t2i.json`](workflows/krea2_turbo_all_in_one_t2i.json) | **1-Click Generation** (single 12.77 GB file) | All-in-One |
|
| 80 |
+
| [`krea2_turbo_svdquant_w4a4_t2i.json`](workflows/krea2_turbo_svdquant_w4a4_t2i.json) | Standard Turbo text-to-image (8 steps, `cfg 1.0`) | 3-File Modular |
|
| 81 |
+
| [`krea2_turbo_svdquant_w4a4_lora.json`](workflows/krea2_turbo_svdquant_w4a4_lora.json) | Turbo with LoRA/LoKr support (`bypass`, `svd delta`, `bake`) | 3-File Modular |
|
| 82 |
+
| [`krea2_base_svdquant_w4a4_t2i.json`](workflows/krea2_base_svdquant_w4a4_t2i.json) | Base non-turbo model (50 steps, `cfg 3.5`) | 3-File Modular |
|
| 83 |
+
| [`krea2_quantize_all_in_one.json`](workflows/krea2_quantize_all_in_one.json) | **Bake your own All-in-One checkpoint** in ComfyUI | Quantizer |
|
| 84 |
+
| [`krea2_quantize.json`](workflows/krea2_quantize.json) | Quantize BF16 DiT to SVDQuant in ComfyUI | Quantizer |
|
| 85 |
+
| [`krea2_quantize_calibrated.json`](workflows/krea2_quantize_calibrated.json) | Live activation capture + quantization | Quantizer |
|
| 86 |
+
| [`krea2_svdquant_diagnostics.json`](workflows/krea2_svdquant_diagnostics.json) | Verify environment, CUDA kernel & memory stats | Diagnostics |
|
| 87 |
|
| 88 |
## See it
|
| 89 |
|
|
|
|
| 167 |
|
| 168 |
## The nodes
|
| 169 |
|
| 170 |
+
Installing this adds nine nodes, all under **Krea2/SVDQuant**:
|
| 171 |
|
| 172 |
| node | what it is for |
|
| 173 |
|---|---|
|
| 174 |
+
| **Krea2 SVDQuant W4A4 Loader** | Loads standalone `svdq` DiT diffusion models |
|
| 175 |
+
| **Krea2 SVDQuant Checkpoint Loader** | Loads unified **All-in-One** checkpoints (DiT + 4-bit Qwen3-VL 4B TE + VAE) |
|
| 176 |
+
| **Krea2 SVDQuant LoRA Loader** | LoRAs, LoKrs, and LoHas on quantized blocks — supports `bypass`, `svd delta`, and `bake` modes |
|
| 177 |
+
| **Krea2 SVDQuant Quantize** | Builds a quantized DiT checkpoint without leaving ComfyUI |
|
| 178 |
+
| **Krea2 SVDQuant Quantize All-in-One** | Bakes DiT, 4-bit text encoder, and VAE into a unified ~12 GB checkpoint directly from ComfyUI |
|
| 179 |
| **Krea2 SVDQuant Diagnostics** | Backend dispatch, memory accounting, per-layer timings, profiler table |
|
| 180 |
| **Krea2 SVDQuant Env Check** | Is the int4 kernel available at all? Needs no model, so you can ask before downloading 8 GB |
|
| 181 |
| **Krea2 SVDQuant Capture Start** / **Capture Save** | Record activation statistics for an activation-aware build |
|
|
|
|
| 186 |
| file | what it is |
|
| 187 |
|---|---|
|
| 188 |
| `quantize_krea2.py` | Converts a BF16 Krea 2 checkpoint to int8, w4a4, or w4a4 + low-rank (svdq). Also the CLI |
|
| 189 |
+
| `svdquant_w4a4.py` | The **W4A4 Loader** & **Checkpoint Loader** nodes — loads standalone or All-In-One `--format svdq` checkpoints |
|
| 190 |
| `svdquant_lora.py` | The **LoRA Loader** node — applies LoRAs as a parallel branch so the 4-bit weight is never dequantized |
|
| 191 |
| `svdquant_quantize.py` | The **Quantize** node — the quantizer above, from inside ComfyUI |
|
| 192 |
| `svdquant_capture.py` | The **Capture Start/Save** nodes — record per-channel activation RMS for `--act-stats` |
|
| 193 |
| `svdquant_diag.py` | The **Diagnostics** and **Env Check** nodes |
|
| 194 |
| `diagnose.py` | The same reports from a terminal, without starting ComfyUI (`--mode all` for everything) |
|
| 195 |
+
| `tools/build_all_in_one.py` | Bakes DiT, 4-bit text encoder (Qwen3-VL 4B), and VAE into a single combined checkpoint (~12 GB) |
|
| 196 |
| `tools/fidelity_bench.py` | Paired multi-seed multi-LoRA LPIPS harness. Every fidelity claim in this repo comes from it |
|
| 197 |
| `tools/speed_bench.py` | Per-step speed, by timing each checkpoint at two step counts and taking the slope. Every s/step number in this repo comes from it |
|
| 198 |
| `tools/contact_sheet.py` | Builds the contact sheets in [GALLERY.md](GALLERY.md) — for looking at, not scoring |
|
|
|
|
| 207 |
|
| 208 |
## Build your own checkpoint
|
| 209 |
|
| 210 |
+
**Without a terminal.** Drag `workflows/krea2_quantize.json` in, point `source_model` at your
|
| 211 |
+
BF16 checkpoint, press Queue. Every widget carries a tooltip and the note on the left covers
|
| 212 |
+
the choices that matter (which format, which rank, how much disk).
|
| 213 |
+
|
| 214 |
+
Fill in `act_stats` while you are there — it is the cheapest quality you will get here and you
|
| 215 |
+
do not have to measure anything for it. Download the file matching your variant from
|
| 216 |
+
[`calibration/`](https://huggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI/tree/main/calibration)
|
| 217 |
+
(6.67 MB) into `ComfyUI/output/` and type its name in: `krea2_act_stats_turbo.safetensors` for Turbo,
|
| 218 |
+
`krea2_act_stats_base.safetensors` for base.
|
| 219 |
+
|
| 220 |
+
**You only need to capture your own statistics for weights those files do not describe** — a
|
| 221 |
+
finetune, a merge, anything that is not the released checkpoint. Activation statistics describe
|
| 222 |
+
the weights they came from, and sharing an architecture is not enough: Turbo's file loads into a
|
| 223 |
+
base build without complaint and then describes the wrong model. `check_act_stats_coverage`
|
| 224 |
+
catches a genuinely foreign model before any GPU work, but it matches on layer *names*, which
|
| 225 |
+
Turbo and base share — so it cannot catch that particular mistake for you. For that case,
|
| 226 |
+
`workflows/krea2_quantize_calibrated.json` runs the capture and the quantize in one Queue press:
|
| 227 |
+
the Capture Save node's `act_stats_path` output feeds the Quantize node, and that wire is also
|
| 228 |
+
what guarantees the calibration runs *first* — without it ComfyUI is free to quantize before
|
| 229 |
+
sampling and you get an uncalibrated file with no warning.
|
| 230 |
+
|
| 231 |
+
Three things to know before you press Queue, in either graph: it blocks the queue for the
|
| 232 |
+
whole run (54 s to ~6 min), it unloads any resident model to take the GPU, and it writes ~8 GB
|
| 233 |
+
(refusing rather than overwriting unless you tick `overwrite`).
|
| 234 |
+
|
| 235 |
From a terminal:
|
| 236 |
|
| 237 |
```bash
|
| 238 |
+
cd ComfyUI/custom_nodes/krea-2-svdquant
|
| 239 |
python quantize_krea2.py /path/to/krea2_bf16.safetensors --format svdq --rank 256 \
|
| 240 |
+
--act-stats krea2_act_stats_turbo.safetensors
|
| 241 |
```
|
| 242 |
|
| 243 |
`--format int8` and `--format w4a4` also work (`int8` is the most faithful option and still
|
|
|
|
| 246 |
the metadata — it does not change the quantization, since layer selection keys off block
|
| 247 |
naming that Turbo and base share.
|
| 248 |
|
| 249 |
+
The **Krea2 SVDQuant Quantize** node calls this same function — the CLI and the graph are one
|
| 250 |
+
quantizer, not two that drift.
|
| 251 |
+
|
| 252 |
+
**Builds are reproducible, per device.** The low-rank split is a *randomized* SVD, so before
|
| 253 |
+
`--seed` existed two runs of the same command on the same GPU produced different checkpoints —
|
| 254 |
+
around 1e-4 per weight, which a sampler then compounds over 8–50 steps into a visibly different
|
| 255 |
+
image. The seed defaults to `0`; pass `--seed -1` (or `-1` in the node) for the old behaviour.
|
| 256 |
+
Across devices it still differs whatever the seed: CPU and CUDA neither draw the same numbers
|
| 257 |
+
nor reduce their GEMMs in the same order. Both the seed and the build device are recorded in
|
| 258 |
+
the checkpoint metadata (`krea2_svdquant_seed`, `krea2_svdquant_device`).
|
| 259 |
|
| 260 |
**`rank` and `refine_iters` are one lever, not two.** With refinement off a rank sweep is flat
|
| 261 |
— rank 16 and rank 256 land within noise of each other — so raising rank without
|
|
|
|
| 338 |
|
| 339 |
</details>
|
| 340 |
|
| 341 |
+
### All-in-One Checkpoint (`tools/build_all_in_one.py`)
|
| 342 |
+
|
| 343 |
+
Bake the diffusion model, text encoder, and VAE into a single file (~12 GB instead of 15.5 GB across three separate files):
|
| 344 |
+
|
| 345 |
+
```bash
|
| 346 |
+
python tools/build_all_in_one.py \
|
| 347 |
+
--dit models/diffusion_models/Krea2-Turbo-SVDQuant-W4A4-rank64.safetensors \
|
| 348 |
+
--text-encoder models/text_encoders/qwen3vl_4b_bf16.safetensors \
|
| 349 |
+
--vae models/vae/Krea2-HD-vae.safetensors \
|
| 350 |
+
--te-format w4a4 --variant turbo
|
| 351 |
+
```
|
| 352 |
+
|
| 353 |
+
* **Streaming Safetensors writer**: never holds the combined 12+ GB in memory simultaneously.
|
| 354 |
+
* **Quantized Text Encoder**: quantizes the 252 linear projections of Qwen3-VL 4B (`self_attn.*_proj`, `mlp.*_proj`) to 4-bit (`convrot_w4a4` or `int8`) while leaving the vision tower and embeddings untouched.
|
| 355 |
+
* **Loading**:
|
| 356 |
+
- `SVDQuant` all-in-one checkpoints load with the **Krea2 SVDQuant Checkpoint Loader** node.
|
| 357 |
+
- Branchless checkpoints (`--format w4a4` or `int8`) load with stock **CheckpointLoaderSimple**.
|
| 358 |
+
* **Workflow**: drag `workflows/krea2_turbo_all_in_one_t2i.json` into ComfyUI for a streamlined 1-loader generation graph.
|
| 359 |
+
|
| 360 |
## What was measured
|
| 361 |
|
| 362 |
Full tables, methodology and caveats: **[BENCHMARKS.md](BENCHMARKS.md)**. Images:
|
|
|
|
| 474 |
|
| 475 |
## Troubleshooting
|
| 476 |
|
| 477 |
+
**[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** covers the things that actually go wrong:
|
| 478 |
generation slower than FP8 (nearly always a pre-cu130 torch build), no speedup at all on
|
| 479 |
+
RTX 20-series, `Pin error.` in the console, iteration time jumping to 30-100 s past ~12 GB
|
| 480 |
+
(fixed — update if you see it), **LoRA renders changing after this release** (measured, and
|
| 481 |
+
the old behaviour was the unstable one), out-of-memory on a small card, LoRAs that match no
|
| 482 |
+
layer, sage-attention artifacts, and "left over keys in diffusion model" after re-saving.
|
| 483 |
|
| 484 |
Start with the **Krea2 SVDQuant Diagnostics** node between the loader and the KSampler, or:
|
| 485 |
|
ROADMAP.md
CHANGED
|
@@ -9,7 +9,78 @@ down gets run twice.
|
|
| 9 |
|
| 10 |
---
|
| 11 |
|
| 12 |
-
## 1.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
**Status:** never tried. `--act-stats` shipped after the rank sweep was run, and only a
|
| 15 |
rank-256 build was ever made. So "is act-aware rank 64 as good as act-aware rank 256" is
|
|
@@ -37,7 +108,7 @@ that rank 256 is "~4% slower than rank 16" — it is 8.2%.)
|
|
| 37 |
|
| 38 |
**Cost:** two builds (~14 min each) plus a `base`-arm fidelity run (~25 min).
|
| 39 |
|
| 40 |
-
##
|
| 41 |
|
| 42 |
**Status:** open bug, one failed attempt.
|
| 43 |
|
|
@@ -67,49 +138,102 @@ see whether Dynamo honours an instance-level `forward` on a *child* module in th
|
|
| 67 |
That separates "Dynamo ignores instance patches on children" from "something in ComfyUI
|
| 68 |
replaces the module".
|
| 69 |
|
| 70 |
-
##
|
| 71 |
|
| 72 |
-
**Status:**
|
| 73 |
-
input, alongside `bypass` and `bake`.
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
| mode | s/step (3090, 1440x1920, r256 + LoRA + LoKr + a `.diff`) | what it costs you |
|
| 78 |
-
|---|---|---|
|
| 79 |
-
| `bypass` (default) | 5.21 | exact, but the adapter runs every forward |
|
| 80 |
-
| `bake` | 3.55 | fast, but the LoKr delta is requantized to **4 bits** |
|
| 81 |
-
| stock loader | 3.22 | same, and the plain LoRA is requantized too |
|
| 82 |
-
|
| 83 |
-
The bypass cost is inherent, not an implementation flaw — ComfyUI's `h()` already does the
|
| 84 |
-
efficient nested contraction and never materialises the Kronecker product
|
| 85 |
-
(`comfy/weight_adapter/lokr.py:146-163`). For a 6144→6144 layer with 4 groups it is ~38.7
|
| 86 |
-
GMAC per layer in **bf16**, against the base layer's 154 GMAC in **int4**. A quarter of the
|
| 87 |
-
arithmetic on hardware that is ~8x slower for it, which is why it roughly doubles the layer.
|
| 88 |
-
|
| 89 |
-
**The idea.** At load time, extract the adapter's weight delta via ComfyUI's own
|
| 90 |
-
`calculate_weight` (applied to a zero weight, so the result is the pure ΔW), take a truncated
|
| 91 |
-
SVD with `torch.svd_lowrank`, and concatenate the factors onto the layer's existing
|
| 92 |
-
`svdq_l1`/`svdq_l2`. Runtime cost then drops to *zero* beyond a slightly wider branch, and the
|
| 93 |
-
delta is **truncated rather than quantized to 4 bits** — the same trade already accepted for
|
| 94 |
-
the base weight, and a much gentler one than `bake` makes.
|
| 95 |
-
|
| 96 |
-
**Open questions:** what rank the delta needs (a Kronecker product is not inherently
|
| 97 |
-
low-rank, so this may need measuring per adapter); ~15 s of SVD at load across 224 layers;
|
| 98 |
-
and whether the widened branch's VRAM is acceptable, given the branch is already 24% of a step
|
| 99 |
-
at rank 256. Wants a fidelity run against `bypass` before it could become the default.
|
| 100 |
-
|
| 101 |
-
## 4. Mixed precision: keep the first and last blocks at W8A8
|
| 102 |
|
| 103 |
**Status:** standard SVDQuant lever, never tried here. Costs file size, buys fidelity. No
|
| 104 |
estimate — nothing has been measured, which is exactly why it is on this list rather than
|
| 105 |
above the items that have been.
|
| 106 |
|
| 107 |
-
##
|
| 108 |
|
| 109 |
`Krea2-Base-SVDQuant-W4A4-rank256-actaware` is published and has **never been through the
|
| 110 |
paired benchmark** (README says so). `tools/fidelity_bench.py --variant base` already
|
| 111 |
supports it. Not a quality lever — a claim with no evidence behind it.
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
---
|
| 114 |
|
| 115 |
## Measured and rejected — do not redo these
|
|
|
|
| 9 |
|
| 10 |
---
|
| 11 |
|
| 12 |
+
## 1. Let the svdq loader use ComfyUI's dynamic patcher
|
| 13 |
+
|
| 14 |
+
**Status: done.** `disable_dynamic=True` is gone from both loaders; they default to whatever
|
| 15 |
+
ComfyUI would do for any other model, which on an ordinary launch is `ModelPatcherDynamic`.
|
| 16 |
+
The `vram_management` input (and `KREA2_DISABLE_DYNAMIC=1`) restores the pin.
|
| 17 |
+
|
| 18 |
+
The pin was waiting on two conditions. Both hold on current ComfyUI, and both were
|
| 19 |
+
measured, not argued from the source:
|
| 20 |
+
|
| 21 |
+
1. **The branch buffers survive the streaming patcher.** `ModelPatcherDynamic.load()` ends
|
| 22 |
+
by walking `self.model.named_buffers(recurse=True)`, moving every buffer to the load
|
| 23 |
+
device with `set_attr_buffer` and stashing the original in `self.backup_buffers`. Unload
|
| 24 |
+
is symmetric: `partially_unload()` falls through to `restore_loaded_backups()`.
|
| 25 |
+
|
| 26 |
+
Measured on a 3090 with `--reserve-vram 15` (~7 GB usable against an 8.51 GiB model),
|
| 27 |
+
read from the diagnostics node *after* a render — which is the only time it means
|
| 28 |
+
anything, since both arms show `cpu` at load time:
|
| 29 |
+
|
| 30 |
+
| `vram_management` | factor devices | weight devices | lowvram |
|
| 31 |
+
|---|---|---|---|
|
| 32 |
+
| `auto` | **448 × `cuda:0`** | 224 × `cpu` | False |
|
| 33 |
+
| `classic` | 368 × `cpu`, 80 × `cuda:0` | 184 × `cpu`, 40 × `cuda:0` | True |
|
| 34 |
+
|
| 35 |
+
So `add_low_rank`'s `cast_to` really is a no-op under `auto` — the opposite of the
|
| 36 |
+
feared per-step multi-MB staging copy — and the division of labour is the one you would
|
| 37 |
+
pick by hand: the small branch every step needs stays resident, the 4-bit weights
|
| 38 |
+
stream.
|
| 39 |
+
2. **`module_size()` still counts them.** `_load_list()` derives its per-module budget from
|
| 40 |
+
`comfy.model_management.module_size`, which sums `state_dict()` — exactly what
|
| 41 |
+
`_publish_in_state_dict` exists to feed. The 1.6 GiB at rank 256 is visible to the
|
| 42 |
+
budget.
|
| 43 |
+
|
| 44 |
+
Two things that also had to be true and are:
|
| 45 |
+
|
| 46 |
+
* The compile fast path self-disables rather than reading a stale weight. Under the dynamic
|
| 47 |
+
patcher the resident copy lives in `_v_weight`/`_prefetch` while `module.weight` stays on
|
| 48 |
+
the host, so `_install_custom_op`'s per-call `weight._qdata.device != x.device` check
|
| 49 |
+
sends the call to the stock forward. Correct, at the cost of the in-graph op — which only
|
| 50 |
+
matters under `TorchCompileModel`.
|
| 51 |
+
* The LoRA node is unaffected: it works through `add_object_patch`, and
|
| 52 |
+
`ModelPatcherDynamic.patch_model` delegates object patches to `super()`.
|
| 53 |
+
|
| 54 |
+
**What is still open:** the buffers are moved with a plain device copy, outside the dynamic
|
| 55 |
+
allocator's `allocated_size` accounting. Real VRAM, invisible bookkeeping. That is why the
|
| 56 |
+
escape hatch shipped with the fix rather than the fix shipping alone.
|
| 57 |
+
|
| 58 |
+
## 1b. Decide which patcher renders a LoRA *correctly*
|
| 59 |
+
|
| 60 |
+
**Status: open, and the only loose end left by the unpin.** Without a LoRA the two patchers
|
| 61 |
+
are bit-identical. With one they are not, and `classic` is additionally not reproducible --
|
| 62 |
+
its output moves with how much of the model was offloaded:
|
| 63 |
+
|
| 64 |
+
| pair | PSNR | SSIM |
|
| 65 |
+
|---|---|---|
|
| 66 |
+
| `auto` vs `auto`, three runs across two VRAM budgets | ∞ | 1.000000 |
|
| 67 |
+
| `classic` full-load vs `classic` under `--reserve-vram 15` | 17.86 dB | 0.712 |
|
| 68 |
+
| `classic` vs `auto` | 15.4-15.9 dB | 0.61-0.65 |
|
| 69 |
+
|
| 70 |
+
The branch bookkeeping is identical in both arms -- 224 quantized layers branched, 32
|
| 71 |
+
patched normally, logged the same either way -- so this is not the node failing to attach
|
| 72 |
+
something. The suspects are the 32 non-quantized layers, which go through ComfyUI's own
|
| 73 |
+
`add_patches`, and the fact that the classic path has two ways of applying a patch (in place
|
| 74 |
+
when resident, `LowVramPatch` at cast time when not) while the dynamic path has one.
|
| 75 |
+
|
| 76 |
+
`auto` being invariant is a good sign and not a proof. **Nothing here establishes which
|
| 77 |
+
image is the faithful one.** What would: apply the same LoRA to a BF16 Krea 2 and compare
|
| 78 |
+
both arms against it with `tools/fidelity_bench.py` -- the harness already does paired
|
| 79 |
+
LPIPS against a BF16 reference, so this is a run, not new machinery. Until that happens the
|
| 80 |
+
honest statement is the one in TROUBLESHOOTING.md: the default changed, the output changed,
|
| 81 |
+
and the new one is at least reproducible.
|
| 82 |
+
|
| 83 |
+
## 2. Act-aware at ranks other than 256
|
| 84 |
|
| 85 |
**Status:** never tried. `--act-stats` shipped after the rank sweep was run, and only a
|
| 86 |
rank-256 build was ever made. So "is act-aware rank 64 as good as act-aware rank 256" is
|
|
|
|
| 108 |
|
| 109 |
**Cost:** two builds (~14 min each) plus a `base`-arm fidelity run (~25 min).
|
| 110 |
|
| 111 |
+
## 3. Make `TorchCompileModel` work on branchless checkpoints
|
| 112 |
|
| 113 |
**Status:** open bug, one failed attempt.
|
| 114 |
|
|
|
|
| 138 |
That separates "Dynamo ignores instance patches on children" from "something in ComfyUI
|
| 139 |
replaces the module".
|
| 140 |
|
| 141 |
+
## 4. Fold LoKr/LoHa/OFT deltas into the low-rank branch by SVD
|
| 142 |
|
| 143 |
+
**Status:** Completed in `experimental/all-in-one`. Added as the `svd delta` mode for `Krea2SVDQuantLoraLoader`, decomposing weight deltas at load time via `torch.svd_lowrank` and merging low-rank factors into the low-rank branch with zero per-step runtime overhead.
|
|
|
|
| 144 |
|
| 145 |
+
## 5. Mixed precision: keep the first and last blocks at W8A8
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
**Status:** standard SVDQuant lever, never tried here. Costs file size, buys fidelity. No
|
| 148 |
estimate — nothing has been measured, which is exactly why it is on this list rather than
|
| 149 |
above the items that have been.
|
| 150 |
|
| 151 |
+
## 6. Benchmark the base checkpoint
|
| 152 |
|
| 153 |
`Krea2-Base-SVDQuant-W4A4-rank256-actaware` is published and has **never been through the
|
| 154 |
paired benchmark** (README says so). `tools/fidelity_bench.py --variant base` already
|
| 155 |
supports it. Not a quality lever — a claim with no evidence behind it.
|
| 156 |
|
| 157 |
+
## 7. All-in-one checkpoint: diffusion + text encoder + VAE in one file
|
| 158 |
+
|
| 159 |
+
**Status:** Completed in `experimental/all-in-one`.
|
| 160 |
+
- Standalone builder: `tools/build_all_in_one.py` (streaming two-pass safetensors assembler).
|
| 161 |
+
- In-Graph node: `Krea2SVDQuantQuantizeAllInOne` in `svdquant_quantize.py`.
|
| 162 |
+
- Loader node: `Krea2SVDQuantCheckpointLoader` in `svdquant_w4a4.py`.
|
| 163 |
+
- Workflows: `workflows/krea2_turbo_all_in_one_t2i.json` and `workflows/krea2_quantize_all_in_one.json`.
|
| 164 |
+
- Quantizes the 252 language projections of Qwen3-VL 4B to 4-bit (`convrot_w4a4`, ~3.2 GB) while keeping the vision tower (315 tensors) and embeddings in native precision for 100% prompt fidelity. Total size: ~11.7–12.3 GB (down from 33.5 GB BF16 / 15.5 GB split).
|
| 165 |
+
|
| 166 |
+
**ComfyUI already supports this, which is the surprise.** `class Krea2` in
|
| 167 |
+
`comfy/supported_models.py` defines `vae_key_prefix = ["vae."]` and
|
| 168 |
+
`text_encoder_key_prefix = ["text_encoders."]`, and its `clip_target()` runs `llama_detect`
|
| 169 |
+
over `text_encoders.qwen3vl_4b.transformer.`, which resolves to
|
| 170 |
+
`comfy.utils.detect_layer_quantization` — a check for `.comfy_quant` markers that is entirely
|
| 171 |
+
format-agnostic. Any layer we quantize with the machinery already in `quantize_krea2.py`
|
| 172 |
+
switches the encoder onto `mixed_ops` and loads. So a **branchless** all-in-one file
|
| 173 |
+
(`--format w4a4` or `int8`) needs no ComfyUI change and no node from this repo: it loads with
|
| 174 |
+
the stock `CheckpointLoaderSimple`.
|
| 175 |
+
|
| 176 |
+
An **svdq** all-in-one does need a node, because the `svdq_l1`/`svdq_l2` buffers have to be
|
| 177 |
+
popped and attached the way `load_svdquant_w4a4` does. That is a wrapper around
|
| 178 |
+
`comfy.sd.load_state_dict_guess_config`, not new mechanism.
|
| 179 |
+
|
| 180 |
+
### The size arithmetic
|
| 181 |
+
|
| 182 |
+
Measured file sizes, not estimates, except where marked:
|
| 183 |
+
|
| 184 |
+
| component | today | all-in-one target |
|
| 185 |
+
|---|---|---|
|
| 186 |
+
| diffusion | 9.78 GB (svdq rank 256) | 8.48 GB (rank 64) or 8.05 GB (noLowRank) |
|
| 187 |
+
| text encoder | 5.24 GB (`qwen3vl_4b_fp8_scaled`) | ~3.3 GB at 4-bit *(estimated)* |
|
| 188 |
+
| VAE | 0.51 GB | 0.51 GB, unquantized |
|
| 189 |
+
| **total** | **15.5 GB across 3 files** | **~11.9–12.3 GB in 1 file** |
|
| 190 |
+
|
| 191 |
+
The 4-bit encoder estimate scales the BF16 encoder (8.88 GB) by the ratio the diffusion model
|
| 192 |
+
gets (24 GB → 8.05 GB, ~34%), and is the number most likely to be wrong: a 4B LLM spends a much
|
| 193 |
+
larger share of itself on embeddings than a DiT does, and embeddings do not quantize here.
|
| 194 |
+
|
| 195 |
+
Note what this does **not** buy: the text encoder and the diffusion model are never resident at
|
| 196 |
+
the same time — ComfyUI encodes, unloads, then samples. So this is a disk, download and
|
| 197 |
+
correctness win, and a VRAM win during the encode pass only. Sampling speed and sampling VRAM
|
| 198 |
+
do not move. Worth saying out loud before anyone reads "12 GB instead of 15.5 GB" as a VRAM
|
| 199 |
+
figure.
|
| 200 |
+
|
| 201 |
+
### What has to be built
|
| 202 |
+
|
| 203 |
+
1. **A second layer set.** `_QUANT_SUFFIXES` targets `blocks.N.{attn,mlp}.*`, which is the DiT.
|
| 204 |
+
The encoder is a Qwen3 LLM: `layers.N.self_attn.{q,k,v,o}_proj`,
|
| 205 |
+
`layers.N.mlp.{gate,up,down}_proj`. `is_target` has to take the set rather than close over
|
| 206 |
+
one.
|
| 207 |
+
2. **Leave the vision tower alone**, at least at first. Qwen3-VL carries one, krea2edit's image
|
| 208 |
+
conditioning path may use it, and quantizing something to find out whether it is dead weight
|
| 209 |
+
is the wrong order of operations.
|
| 210 |
+
3. **A combiner**, streaming three state dicts into one with the prefixes above. The BF16 source
|
| 211 |
+
is 24 GB and the encoder another 8.88 GB, so it has to stream the way `convert()` already
|
| 212 |
+
does rather than build the dict in memory.
|
| 213 |
+
4. **An svdq checkpoint loader node**, if the svdq variant is wanted.
|
| 214 |
+
|
| 215 |
+
### The risk, and how to measure it
|
| 216 |
+
|
| 217 |
+
4-bit is a much bigger ask of an LLM than of this DiT. The DiT tolerates it because convrot
|
| 218 |
+
spreads the outliers and the low-rank branch absorbs what is left; LLM activations have
|
| 219 |
+
outlier channels severe enough that a whole literature (SmoothQuant, AWQ) exists about them.
|
| 220 |
+
INT8 is not an alternative — it is the same byte count as the FP8 encoder we already ship, so
|
| 221 |
+
4-bit is the only setting that changes the number.
|
| 222 |
+
|
| 223 |
+
Failure will not look like noise. It will look like **prompt adherence quietly getting worse**:
|
| 224 |
+
a dropped clause, a colour that drifts, a count that stops being respected. That is exactly the
|
| 225 |
+
kind of regression the existing harness catches, because it is measured against a BF16
|
| 226 |
+
reference at fixed seeds — hold the diffusion model constant, swap only the encoder, and run
|
| 227 |
+
`tools/fidelity_bench.py`. The dense-text and counting prompts in the benchmark set are the
|
| 228 |
+
ones to read first.
|
| 229 |
+
|
| 230 |
+
Act-aware calibration on the encoder is the natural follow-up if plain 4-bit is close but not
|
| 231 |
+
close enough. It needs its own capture: the hooks would ride on `CLIPTextEncode`, not on the
|
| 232 |
+
sampler, so `svdquant_capture.py` needs a second pair of nodes rather than a flag. Do not build
|
| 233 |
+
that before the plain 4-bit measurement says whether it is needed.
|
| 234 |
+
|
| 235 |
+
---
|
| 236 |
+
|
| 237 |
---
|
| 238 |
|
| 239 |
## Measured and rejected — do not redo these
|
TROUBLESHOOTING.md
CHANGED
|
@@ -122,20 +122,109 @@ locked pages aggressively — `MAX_PINNED_MEMORY` there is 40% of system RAM —
|
|
| 122 |
routinely with a model this size. It is not specific to `svdq`; INT8 checkpoints trigger it
|
| 123 |
too. The diagnostics node prints your pinned-memory budget under `mode=env`.
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
## Out of memory on a small card (and int8 works fine)
|
| 126 |
|
| 127 |
Fixed. The low-rank factors were attached as non-persistent buffers, which ComfyUI's
|
| 128 |
`module_size()` — the basis of every VRAM decision, including the lowvram split — could
|
| 129 |
not see, while `.to(device)` moved them anyway. Worse, the old branch cached its own
|
| 130 |
device move back onto the module, so once ComfyUI offloaded a layer the factors quietly
|
| 131 |
-
came back to the GPU and stayed there, outside all accounting.
|
| 132 |
-
which is the difference between fitting and not on an 8 GB card. INT8
|
| 133 |
-
branch, so they were never affected.
|
| 134 |
|
| 135 |
They are now published into `state_dict()` under their own `svdq_l1` / `svdq_l2` keys and
|
| 136 |
staged per call via `comfy.model_management.cast_to`, so they are budgeted and offloaded
|
| 137 |
-
like any other weight.
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
One gap remains and it is upstream, not here: `QuantizedTensor.nbytes` reports only the
|
| 141 |
packed weight, so the W4A4 `weight_scale` (~3 MB/layer) is still invisible to ComfyUI's
|
|
|
|
| 122 |
routinely with a model this size. It is not specific to `svdq`; INT8 checkpoints trigger it
|
| 123 |
too. The diagnostics node prints your pinned-memory budget under `mode=env`.
|
| 124 |
|
| 125 |
+
## Iteration time jumps to 30-100 s once VRAM passes ~12 GB (and FP8/INT8 do not)
|
| 126 |
+
|
| 127 |
+
**Fixed.** If you are on a build that still does this, update.
|
| 128 |
+
|
| 129 |
+
The `svdq` loader used to opt the model out of ComfyUI's dynamic VRAM management:
|
| 130 |
+
`load_svdquant_w4a4` passed `disable_dynamic=True`, which pinned it to the classic
|
| 131 |
+
`ModelPatcher`. The stock `UNETLoader` does not do that, so the FP8, INT8 and `noLowRank`
|
| 132 |
+
checkpoints got the streaming patcher and the `svdq` ones did not. Past the point where the
|
| 133 |
+
model no longer fits, the pinned path falls back to streaming weights per module per step,
|
| 134 |
+
and a ~1 s iteration becomes 30-100 s. Nothing about the quantization was involved — it was
|
| 135 |
+
which patcher the loader asked for.
|
| 136 |
+
|
| 137 |
+
Both loaders now default to whatever ComfyUI would do for any other model, which on an
|
| 138 |
+
ordinary launch is the dynamic patcher. The **status output** on the loader node says which
|
| 139 |
+
one you got:
|
| 140 |
+
|
| 141 |
+
```
|
| 142 |
+
w4a4 + low-rank: attached 224 branches (rank 256, variant turbo), model_size 9.10 GiB,
|
| 143 |
+
ModelPatcherDynamic (dynamic vram: on), compile: 224 layers in-graph via krea2::w4a4_linear
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
`ModelPatcher (dynamic vram: off)` there means you are on the old path — either you set
|
| 147 |
+
`vram_management` to `classic`, or ComfyUI itself has dynamic VRAM off (`--highvram`,
|
| 148 |
+
`--novram`, `--gpu-only`, `--cpu`, `--disable-dynamic-vram`, or no working `comfy-aimdo`
|
| 149 |
+
install; the startup log says `DynamicVRAM support detected and enabled` when it is on).
|
| 150 |
+
|
| 151 |
+
### If you need the old behaviour back
|
| 152 |
+
|
| 153 |
+
One thing here is genuinely unmeasured: the streaming patcher moves the low-rank branch
|
| 154 |
+
buffers with a plain device copy rather than through its own allocator, so at rank 256 there
|
| 155 |
+
is 1.6 GiB of real VRAM its internal bookkeeping does not account for. If that turns out to
|
| 156 |
+
bite on your card, set the loader's **`vram_management`** input to `classic`, or run the
|
| 157 |
+
whole server with `KREA2_DISABLE_DYNAMIC=1`. Please open an issue if you have to — that is
|
| 158 |
+
the case this escape hatch exists to hear about.
|
| 159 |
+
|
| 160 |
+
### Still tight on a 12 GB card
|
| 161 |
+
|
| 162 |
+
The branch is real memory on top of the checkpoint — measured off the published files,
|
| 163 |
+
1.6 GiB at rank 256 and 0.4 GiB at rank 64 across the 224 blocks — and the rank-256 build is
|
| 164 |
+
9.10 GB against 7.50 GB for `noLowRank`. So the older advice still holds as a *size*
|
| 165 |
+
argument, just no longer as a workaround for a 30x slowdown:
|
| 166 |
+
|
| 167 |
+
1. **`Krea2-Turbo-W4A4-noLowRank.safetensors`** (7.50 GB) is ~9% faster per step than any
|
| 168 |
+
rank, and gives up the low-rank branch's fidelity.
|
| 169 |
+
2. **`Krea2-Turbo-SVDQuant-W4A4-rank64.safetensors`** (7.90 GB) is 1.2 GB smaller than rank
|
| 170 |
+
256 and statistically identical to it *as long as you never load a LoRA*.
|
| 171 |
+
3. Watch the loader's `model_size` line — it reports the real number, branch included.
|
| 172 |
+
|
| 173 |
+
## My LoRA renders changed after updating (and the image moves when VRAM gets tight)
|
| 174 |
+
|
| 175 |
+
Measured, real, and only half explained. Read this before concluding a LoRA is broken.
|
| 176 |
+
|
| 177 |
+
With **no LoRA loaded**, the two patchers are bit-identical — same seed, same graph, same
|
| 178 |
+
pixels, verified at three different VRAM budgets. With a **LoRA loaded** they are not:
|
| 179 |
+
|
| 180 |
+
| pair | PSNR | SSIM |
|
| 181 |
+
|---|---|---|
|
| 182 |
+
| `auto` vs `auto`, different VRAM budgets, three runs | ∞ | 1.000000 |
|
| 183 |
+
| `classic` full-load vs `classic` under pressure | 17.86 dB | 0.712 |
|
| 184 |
+
| `classic` vs `auto` | 15.4-15.9 dB | 0.61-0.65 |
|
| 185 |
+
|
| 186 |
+
So the instability is on the `classic` side: it renders a LoRA differently depending on how
|
| 187 |
+
much of the model got offloaded, because ComfyUI's classic path applies patches through
|
| 188 |
+
`LowVramPatch` at cast time when a layer is not resident and in place when it is. The
|
| 189 |
+
dynamic patcher has one path and uses it always, which is why all three `auto` runs agree
|
| 190 |
+
to the bit.
|
| 191 |
+
|
| 192 |
+
Until this release the loader pinned to `classic`, so **your previous LoRA output was the
|
| 193 |
+
classic one** — and, if you were near your VRAM limit, not necessarily reproducible either.
|
| 194 |
+
Now the default is `auto` and the result is stable. It is also a different image.
|
| 195 |
+
|
| 196 |
+
**What has not been established: which one is numerically right.** Determinism is not
|
| 197 |
+
correctness. Settling it needs the same LoRA applied to a reference the quantized path can
|
| 198 |
+
be compared against, and that reference does not exist here yet — see
|
| 199 |
+
[ROADMAP.md](ROADMAP.md). If you preferred the old look, `vram_management=classic` on the
|
| 200 |
+
loader node reproduces it, with the pressure-dependence that comes with it.
|
| 201 |
+
|
| 202 |
## Out of memory on a small card (and int8 works fine)
|
| 203 |
|
| 204 |
Fixed. The low-rank factors were attached as non-persistent buffers, which ComfyUI's
|
| 205 |
`module_size()` — the basis of every VRAM decision, including the lowvram split — could
|
| 206 |
not see, while `.to(device)` moved them anyway. Worse, the old branch cached its own
|
| 207 |
device move back onto the module, so once ComfyUI offloaded a layer the factors quietly
|
| 208 |
+
came back to the GPU and stayed there, outside all accounting. 0.4 GiB at rank 64 and
|
| 209 |
+
1.6 GiB at rank 256, which is the difference between fitting and not on an 8 GB card. INT8
|
| 210 |
+
checkpoints carry no branch, so they were never affected.
|
| 211 |
|
| 212 |
They are now published into `state_dict()` under their own `svdq_l1` / `svdq_l2` keys and
|
| 213 |
staged per call via `comfy.model_management.cast_to`, so they are budgeted and offloaded
|
| 214 |
+
like any other weight.
|
| 215 |
+
|
| 216 |
+
`mode=env` on the diagnostics node reports the factor devices, and **what you should see
|
| 217 |
+
depends on which patcher you are on** — read it after a render, not before, because at load
|
| 218 |
+
time both arms say `cpu`:
|
| 219 |
+
|
| 220 |
+
| `vram_management` | healthy `factor devices` |
|
| 221 |
+
|---|---|
|
| 222 |
+
| `auto` (dynamic, the default) | all on `cuda:0` — 448 of them at rank 256 |
|
| 223 |
+
| `classic` | mostly `cpu`, staged per call |
|
| 224 |
+
|
| 225 |
+
`cuda` under `auto` is not the old bug coming back; it is the streaming patcher keeping the
|
| 226 |
+
small always-needed branch resident and streaming the 4-bit weights instead, which is what
|
| 227 |
+
makes it fast. Measured both ways on a 3090 under `--reserve-vram 15`.
|
| 228 |
|
| 229 |
One gap remains and it is upstream, not here: `QuantizedTensor.nbytes` reports only the
|
| 230 |
packed weight, so the W4A4 `weight_scale` (~3 MB/layer) is still invisible to ComfyUI's
|
__init__.py
CHANGED
|
@@ -1,27 +1,27 @@
|
|
| 1 |
-
from .svdquant_capture import (
|
| 2 |
-
NODE_CLASS_MAPPINGS as _CAPTURE_NODES,
|
| 3 |
-
NODE_DISPLAY_NAME_MAPPINGS as _CAPTURE_NAMES,
|
| 4 |
-
)
|
| 5 |
-
from .svdquant_diag import (
|
| 6 |
-
NODE_CLASS_MAPPINGS as _DIAG_NODES,
|
| 7 |
-
NODE_DISPLAY_NAME_MAPPINGS as _DIAG_NAMES,
|
| 8 |
-
)
|
| 9 |
-
from .svdquant_lora import (
|
| 10 |
-
NODE_CLASS_MAPPINGS as _LORA_NODES,
|
| 11 |
-
NODE_DISPLAY_NAME_MAPPINGS as _LORA_NAMES,
|
| 12 |
-
)
|
| 13 |
-
from .svdquant_quantize import (
|
| 14 |
-
NODE_CLASS_MAPPINGS as _QUANT_NODES,
|
| 15 |
-
NODE_DISPLAY_NAME_MAPPINGS as _QUANT_NAMES,
|
| 16 |
-
)
|
| 17 |
-
from .svdquant_w4a4 import (
|
| 18 |
-
NODE_CLASS_MAPPINGS as _W4A4_NODES,
|
| 19 |
-
NODE_DISPLAY_NAME_MAPPINGS as _W4A4_NAMES,
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
NODE_CLASS_MAPPINGS = {**_LORA_NODES, **_W4A4_NODES, **_DIAG_NODES, **_QUANT_NODES,
|
| 23 |
-
**_CAPTURE_NODES}
|
| 24 |
-
NODE_DISPLAY_NAME_MAPPINGS = {**_LORA_NAMES, **_W4A4_NAMES, **_DIAG_NAMES, **_QUANT_NAMES,
|
| 25 |
-
**_CAPTURE_NAMES}
|
| 26 |
-
|
| 27 |
-
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
|
|
|
| 1 |
+
from .svdquant_capture import (
|
| 2 |
+
NODE_CLASS_MAPPINGS as _CAPTURE_NODES,
|
| 3 |
+
NODE_DISPLAY_NAME_MAPPINGS as _CAPTURE_NAMES,
|
| 4 |
+
)
|
| 5 |
+
from .svdquant_diag import (
|
| 6 |
+
NODE_CLASS_MAPPINGS as _DIAG_NODES,
|
| 7 |
+
NODE_DISPLAY_NAME_MAPPINGS as _DIAG_NAMES,
|
| 8 |
+
)
|
| 9 |
+
from .svdquant_lora import (
|
| 10 |
+
NODE_CLASS_MAPPINGS as _LORA_NODES,
|
| 11 |
+
NODE_DISPLAY_NAME_MAPPINGS as _LORA_NAMES,
|
| 12 |
+
)
|
| 13 |
+
from .svdquant_quantize import (
|
| 14 |
+
NODE_CLASS_MAPPINGS as _QUANT_NODES,
|
| 15 |
+
NODE_DISPLAY_NAME_MAPPINGS as _QUANT_NAMES,
|
| 16 |
+
)
|
| 17 |
+
from .svdquant_w4a4 import (
|
| 18 |
+
NODE_CLASS_MAPPINGS as _W4A4_NODES,
|
| 19 |
+
NODE_DISPLAY_NAME_MAPPINGS as _W4A4_NAMES,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
NODE_CLASS_MAPPINGS = {**_LORA_NODES, **_W4A4_NODES, **_DIAG_NODES, **_QUANT_NODES,
|
| 23 |
+
**_CAPTURE_NODES}
|
| 24 |
+
NODE_DISPLAY_NAME_MAPPINGS = {**_LORA_NAMES, **_W4A4_NAMES, **_DIAG_NAMES, **_QUANT_NAMES,
|
| 25 |
+
**_CAPTURE_NAMES}
|
| 26 |
+
|
| 27 |
+
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
build_all_in_one.py
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bake the diffusion model, the text encoder and the VAE into one checkpoint.
|
| 2 |
+
|
| 3 |
+
The mechanism. `tools/build_all_in_one.py` is the command-line front end for it and
|
| 4 |
+
`Krea2SVDQuantQuantizeAllInOne` (svdquant_quantize.py) is the in-graph one; this module
|
| 5 |
+
lives beside `quantize_krea2.py` rather than under `tools/` because `.comfyignore`
|
| 6 |
+
keeps `tools/` out of the published package and the node has to be able to import it.
|
| 7 |
+
|
| 8 |
+
python tools/build_all_in_one.py \\
|
| 9 |
+
--dit D:/ComfyUI/models/diffusion_models/turbo.safetensors \\
|
| 10 |
+
--text-encoder D:/ComfyUI/models/text_encoders/qwen3vl_4b_bf16.safetensors \\
|
| 11 |
+
--vae D:/ComfyUI/models/vae/Krea2-HD-vae.safetensors \\
|
| 12 |
+
--format svdq --rank 64 --te-format w4a4 --variant turbo \\
|
| 13 |
+
--act-stats calibration/krea2_act_stats_turbo.safetensors
|
| 14 |
+
|
| 15 |
+
Three downloads that have to match becomes one file. `--dry-run` does the whole key mapping
|
| 16 |
+
and prints the size arithmetic without quantizing anything or touching the GPU, which is the
|
| 17 |
+
right way to check the layout before spending ten minutes on a build.
|
| 18 |
+
|
| 19 |
+
WHAT COMFYUI EXPECTS, AND WHY THIS IS MOSTLY A RENAMING PROBLEM
|
| 20 |
+
|
| 21 |
+
`comfy/supported_models.py`'s `Krea2` already declares `vae_key_prefix = ["vae."]` and
|
| 22 |
+
`text_encoder_key_prefix = ["text_encoders."]`, and its `clip_target()` runs
|
| 23 |
+
`detect_layer_quantization` over the encoder prefix -- a format-agnostic scan for
|
| 24 |
+
`.comfy_quant` markers. So a quantized text encoder inside a combined checkpoint is a case
|
| 25 |
+
ComfyUI already handles; nothing upstream needs changing. The three maps:
|
| 26 |
+
|
| 27 |
+
diffusion blocks.N... -> model.diffusion_model.blocks.N...
|
| 28 |
+
vae conv1... / encoder... -> vae.conv1... / vae.encoder...
|
| 29 |
+
encoder model.language_model.X -> text_encoders.qwen3vl_4b.transformer.model.X
|
| 30 |
+
model.visual.X -> text_encoders.qwen3vl_4b.transformer.visual.X
|
| 31 |
+
lm_head.X -> text_encoders.qwen3vl_4b.transformer.model.lm_head.X
|
| 32 |
+
|
| 33 |
+
The encoder map is not invented here: it is the same `state_dict_prefix_replace` call
|
| 34 |
+
`comfy/sd.py` makes in `load_text_encoder_state_dicts` when you load that file through
|
| 35 |
+
`CLIPLoader`. Standalone loading applies it at load time; a combined checkpoint has to carry
|
| 36 |
+
the keys already converted, because `load_state_dict_guess_config` does not repeat it.
|
| 37 |
+
|
| 38 |
+
WHICH FORMATS
|
| 39 |
+
|
| 40 |
+
The diffusion side takes any `--format` `quantize_krea2.py` supports. `svdq` gives the best
|
| 41 |
+
fidelity but the `svdq_l1`/`svdq_l2` buffers need this repo's loader to attach them, so an
|
| 42 |
+
svdq all-in-one needs the Krea2 SVDQuant Checkpoint Loader node rather than the stock
|
| 43 |
+
`CheckpointLoaderSimple`. Build with `--format w4a4` or `int8` for a file that loads with
|
| 44 |
+
stock ComfyUI and no custom node at all.
|
| 45 |
+
|
| 46 |
+
The text encoder takes `--te-format w4a4` or `int8` only, and never a low-rank branch. Not a
|
| 47 |
+
limitation of the quantizer -- `load_state_dict_guess_config` builds the CLIP through
|
| 48 |
+
ComfyUI's own path, which has nowhere to hang a branch. 4-bit is the only setting that
|
| 49 |
+
changes the number worth changing: INT8 is one byte per weight, the same as the FP8 encoder
|
| 50 |
+
already published.
|
| 51 |
+
|
| 52 |
+
WHAT THIS DOES NOT BUY
|
| 53 |
+
|
| 54 |
+
The text encoder and the diffusion model are never resident at the same time -- ComfyUI
|
| 55 |
+
encodes the prompt, unloads, then samples. This is a disk, download and don't-mismatch-your-
|
| 56 |
+
files win, plus a VRAM win during the encode pass. Sampling speed and sampling VRAM do not
|
| 57 |
+
move at all.
|
| 58 |
+
"""
|
| 59 |
+
from __future__ import annotations
|
| 60 |
+
|
| 61 |
+
import json
|
| 62 |
+
import os
|
| 63 |
+
import struct
|
| 64 |
+
import time
|
| 65 |
+
|
| 66 |
+
import torch
|
| 67 |
+
from safetensors import safe_open
|
| 68 |
+
|
| 69 |
+
# Relative when imported as part of the pack, absolute when `tools/build_all_in_one.py` runs
|
| 70 |
+
# this as a plain script. The relative arm is the one that matters: importing
|
| 71 |
+
# `quantize_krea2` absolutely from inside the package creates a *second* top-level copy of
|
| 72 |
+
# it, with its own module-level state, alongside the pack's own `.quantize_krea2`.
|
| 73 |
+
#
|
| 74 |
+
# Branching on `__package__` rather than try/except ImportError so that a real failure
|
| 75 |
+
# inside `quantize_krea2` -- a missing comfy, a broken quant backend -- surfaces as itself
|
| 76 |
+
# instead of being retried down the other path and reported as the wrong problem.
|
| 77 |
+
if __package__:
|
| 78 |
+
from .quantize_krea2 import (
|
| 79 |
+
DEFAULT_SEED,
|
| 80 |
+
REFINE_TOL,
|
| 81 |
+
__version__,
|
| 82 |
+
conf_tensor,
|
| 83 |
+
detect_prefix,
|
| 84 |
+
quantize_weight,
|
| 85 |
+
resolve_format,
|
| 86 |
+
)
|
| 87 |
+
else: # run outside the package by tools/build_all_in_one.py, which puts it on sys.path
|
| 88 |
+
from quantize_krea2 import (
|
| 89 |
+
DEFAULT_SEED,
|
| 90 |
+
REFINE_TOL,
|
| 91 |
+
__version__,
|
| 92 |
+
conf_tensor,
|
| 93 |
+
detect_prefix,
|
| 94 |
+
quantize_weight,
|
| 95 |
+
resolve_format,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
# `model.` here is the encoder's own `model.`, i.e. what `model.language_model.` becomes.
|
| 99 |
+
TE_PREFIX = "text_encoders.qwen3vl_4b.transformer."
|
| 100 |
+
DIT_PREFIX = "model.diffusion_model."
|
| 101 |
+
VAE_PREFIX = "vae."
|
| 102 |
+
|
| 103 |
+
# Exactly `comfy/sd.py`'s `load_text_encoder_state_dicts` mapping for a Qwen3-VL file. Order
|
| 104 |
+
# matters: `model.language_model.` has to be tried before any shorter `model.` rule would be.
|
| 105 |
+
TE_KEY_MAP = (
|
| 106 |
+
("model.language_model.", "model."),
|
| 107 |
+
("model.visual.", "visual."),
|
| 108 |
+
("lm_head.", "model.lm_head."),
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# The 7 projections in a Qwen3 decoder layer. Deliberately not the DiT's `_QUANT_SUFFIXES`:
|
| 112 |
+
# different architecture, different names, and sharing one tuple between them would make a
|
| 113 |
+
# typo in either silently quantize nothing in the other.
|
| 114 |
+
TE_QUANT_SUFFIXES = ("self_attn.q_proj", "self_attn.k_proj", "self_attn.v_proj",
|
| 115 |
+
"self_attn.o_proj", "mlp.gate_proj", "mlp.up_proj", "mlp.down_proj")
|
| 116 |
+
TE_LAYERS = 36
|
| 117 |
+
TE_EXPECTED = TE_LAYERS * len(TE_QUANT_SUFFIXES)
|
| 118 |
+
|
| 119 |
+
# Formats with no low-rank branch. The text encoder loads through ComfyUI's own CLIP path,
|
| 120 |
+
# which has nowhere to attach one.
|
| 121 |
+
TE_FORMATS = {"w4a4": "convrot_w4a4", "int8": "int8_tensorwise"}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def te_is_target(key: str) -> bool:
|
| 125 |
+
"""True for a decoder-layer projection weight, in *converted* key space.
|
| 126 |
+
|
| 127 |
+
Converted, not raw: `model.visual.` becomes `visual.`, and the vision tower has layers
|
| 128 |
+
and projections of its own. Matching after the rename is what keeps them out -- Krea 2's
|
| 129 |
+
text encoder instantiates the vision tower (315 tensors, 0.83 GB) and krea2edit's image
|
| 130 |
+
conditioning may go through it, so it is carried unquantized rather than dropped.
|
| 131 |
+
"""
|
| 132 |
+
if not key.startswith("model.layers.") or not key.endswith(".weight"):
|
| 133 |
+
return False
|
| 134 |
+
return key[: -len(".weight")].endswith(TE_QUANT_SUFFIXES)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def remap_te_key(key: str) -> str:
|
| 138 |
+
for old, new in TE_KEY_MAP:
|
| 139 |
+
if key.startswith(old):
|
| 140 |
+
return new + key[len(old):]
|
| 141 |
+
return key
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
_DTYPE_STR = {
|
| 145 |
+
torch.float32: "F32", torch.float16: "F16", torch.bfloat16: "BF16",
|
| 146 |
+
torch.float64: "F64", torch.int64: "I64", torch.int32: "I32", torch.int16: "I16",
|
| 147 |
+
torch.int8: "I8", torch.uint8: "U8", torch.bool: "BOOL",
|
| 148 |
+
torch.float8_e4m3fn: "F8_E4M3", torch.float8_e5m2: "F8_E5M2",
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class StreamingWriter:
|
| 153 |
+
"""A safetensors writer that never holds the whole checkpoint in memory.
|
| 154 |
+
|
| 155 |
+
`safetensors.torch.save_file` takes a dict, so writing a 12 GB combined checkpoint that
|
| 156 |
+
way needs 12 GB of RAM on top of whatever the quantizer is using. The format does not
|
| 157 |
+
require that: it is an 8-byte header length, a JSON header of name -> {dtype, shape,
|
| 158 |
+
offsets}, then the raw buffers back to back.
|
| 159 |
+
|
| 160 |
+
So this is two passes. `plan()` collects names, dtypes and shapes -- which are known
|
| 161 |
+
without materialising anything -- and `write()` streams the tensors past in the same
|
| 162 |
+
order. The cost is that every tensor is produced twice or held once; here the shapes come
|
| 163 |
+
from safetensors headers and quantizer outputs we already have, so it is neither.
|
| 164 |
+
"""
|
| 165 |
+
|
| 166 |
+
def __init__(self, path: str, metadata: dict | None = None):
|
| 167 |
+
self.path = path
|
| 168 |
+
self.metadata = metadata or {}
|
| 169 |
+
self.entries = [] # (name, dtype_str, shape, nbytes)
|
| 170 |
+
self._names = set()
|
| 171 |
+
|
| 172 |
+
def plan(self, name: str, dtype, shape) -> None:
|
| 173 |
+
if name in self._names:
|
| 174 |
+
raise RuntimeError("duplicate key in the combined checkpoint: {}".format(name))
|
| 175 |
+
self._names.add(name)
|
| 176 |
+
dtype_str = _DTYPE_STR.get(dtype)
|
| 177 |
+
if dtype_str is None:
|
| 178 |
+
raise RuntimeError("no safetensors name for dtype {}".format(dtype))
|
| 179 |
+
nbytes = 1
|
| 180 |
+
for d in shape:
|
| 181 |
+
nbytes *= d
|
| 182 |
+
nbytes *= torch.empty((), dtype=dtype).element_size()
|
| 183 |
+
self.entries.append((name, dtype_str, list(shape), nbytes))
|
| 184 |
+
|
| 185 |
+
def header(self) -> bytes:
|
| 186 |
+
header, offset = {}, 0
|
| 187 |
+
for name, dtype_str, shape, nbytes in self.entries:
|
| 188 |
+
header[name] = {"dtype": dtype_str, "shape": shape,
|
| 189 |
+
"data_offsets": [offset, offset + nbytes]}
|
| 190 |
+
offset += nbytes
|
| 191 |
+
if self.metadata:
|
| 192 |
+
header["__metadata__"] = {k: str(v) for k, v in self.metadata.items()}
|
| 193 |
+
blob = json.dumps(header, separators=(",", ":")).encode("utf-8")
|
| 194 |
+
# The spec wants the buffer 8-byte aligned; pad the header with spaces, which JSON
|
| 195 |
+
# ignores, rather than the buffer, whose offsets are already committed.
|
| 196 |
+
pad = (-len(blob)) % 8
|
| 197 |
+
return blob + b" " * pad
|
| 198 |
+
|
| 199 |
+
def __enter__(self):
|
| 200 |
+
self._blob = self.header()
|
| 201 |
+
self._fh = open(self.path, "wb")
|
| 202 |
+
self._fh.write(struct.pack("<Q", len(self._blob)))
|
| 203 |
+
self._fh.write(self._blob)
|
| 204 |
+
self._i = 0
|
| 205 |
+
return self
|
| 206 |
+
|
| 207 |
+
def write(self, name: str, tensor: torch.Tensor) -> None:
|
| 208 |
+
expected = self.entries[self._i]
|
| 209 |
+
if name != expected[0]:
|
| 210 |
+
raise RuntimeError("write order does not match the plan: expected {!r}, got {!r}"
|
| 211 |
+
.format(expected[0], name))
|
| 212 |
+
t = tensor.contiguous().cpu()
|
| 213 |
+
# Shape and dtype, not just the byte count. The header is written from the *plan*,
|
| 214 |
+
# so a plan that disagrees with the tensor produces a file whose data is right and
|
| 215 |
+
# whose header lies -- and readers believe the header. A byte check does not catch
|
| 216 |
+
# it: [4096] and [4096, 1] are the same 16384 bytes, which is exactly how a
|
| 217 |
+
# text encoder shipped with its scales declared 2-D when convrot_w4a4 returns them
|
| 218 |
+
# 1-D, and failed only later, inside a kernel, as "scales: shape [4096, 1] fails:
|
| 219 |
+
# exactly 1D".
|
| 220 |
+
if list(t.shape) != expected[2]:
|
| 221 |
+
raise RuntimeError("{}: planned shape {}, got {}".format(
|
| 222 |
+
name, expected[2], list(t.shape)))
|
| 223 |
+
planned_dtype = _DTYPE_STR.get(t.dtype)
|
| 224 |
+
if planned_dtype != expected[1]:
|
| 225 |
+
raise RuntimeError("{}: planned dtype {}, got {}".format(
|
| 226 |
+
name, expected[1], planned_dtype))
|
| 227 |
+
buf = t.view(torch.uint8).reshape(-1) if t.dtype not in (torch.uint8,) else t.reshape(-1)
|
| 228 |
+
data = buf.numpy().tobytes()
|
| 229 |
+
if len(data) != expected[3]:
|
| 230 |
+
raise RuntimeError("{}: planned {} bytes, got {}".format(name, expected[3], len(data)))
|
| 231 |
+
self._fh.write(data)
|
| 232 |
+
self._i += 1
|
| 233 |
+
|
| 234 |
+
def __exit__(self, *exc):
|
| 235 |
+
self._fh.close()
|
| 236 |
+
if exc[0] is None and self._i != len(self.entries):
|
| 237 |
+
raise RuntimeError("planned {} tensors but wrote {}".format(len(self.entries), self._i))
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def plan_dit(handle, keys, prefix):
|
| 241 |
+
"""(combined key, source key) for every diffusion tensor, already prefixed."""
|
| 242 |
+
for key in keys:
|
| 243 |
+
bare = key[len(prefix):] if prefix and key.startswith(prefix) else key
|
| 244 |
+
yield DIT_PREFIX + bare, key
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def plan_vae(keys):
|
| 248 |
+
for key in keys:
|
| 249 |
+
yield VAE_PREFIX + key, key
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def quantize_text_encoder(path: str, fmt: str, groupsize: int, device: str, writer,
|
| 253 |
+
dry_run: bool):
|
| 254 |
+
"""Plan (and on the second pass, write) the text encoder side.
|
| 255 |
+
|
| 256 |
+
Returns (planned entries, quantized layer count). The two passes run the same key walk so
|
| 257 |
+
the plan and the write cannot disagree about order -- which is the one way a streaming
|
| 258 |
+
writer produces a file that is subtly, unreadably wrong.
|
| 259 |
+
"""
|
| 260 |
+
algo = TE_FORMATS[fmt]
|
| 261 |
+
quantized = 0
|
| 262 |
+
with safe_open(path, framework="pt", device="cpu") as handle:
|
| 263 |
+
keys = list(handle.keys())
|
| 264 |
+
for key in keys:
|
| 265 |
+
converted = remap_te_key(key)
|
| 266 |
+
out_key = TE_PREFIX + converted
|
| 267 |
+
if not te_is_target(converted):
|
| 268 |
+
if dry_run:
|
| 269 |
+
slice_ = handle.get_slice(key)
|
| 270 |
+
writer.plan(out_key, _slice_dtype(slice_), slice_.get_shape())
|
| 271 |
+
else:
|
| 272 |
+
writer.write(out_key, handle.get_tensor(key))
|
| 273 |
+
continue
|
| 274 |
+
|
| 275 |
+
layer = out_key[: -len(".weight")]
|
| 276 |
+
if dry_run:
|
| 277 |
+
slice_ = handle.get_slice(key)
|
| 278 |
+
shape = slice_.get_shape()
|
| 279 |
+
# int4 packs two values per byte along the input dimension; int8 is 1:1.
|
| 280 |
+
packed = [shape[0], shape[1] // 2] if fmt == "w4a4" else list(shape)
|
| 281 |
+
writer.plan(out_key, torch.int8, packed)
|
| 282 |
+
writer.plan(layer + ".weight_scale", torch.float32,
|
| 283 |
+
_scale_shape(algo, shape[0]))
|
| 284 |
+
writer.plan(layer + ".comfy_quant", torch.uint8, [_conf_len(algo, groupsize)])
|
| 285 |
+
else:
|
| 286 |
+
w = handle.get_tensor(key).to(device=device, dtype=torch.bfloat16)
|
| 287 |
+
qdata, scales, conf = quantize_weight(w, algo, groupsize)
|
| 288 |
+
writer.write(out_key, qdata)
|
| 289 |
+
for name, value in scales.items():
|
| 290 |
+
writer.write("{}.{}".format(layer, name), value)
|
| 291 |
+
writer.write(layer + ".comfy_quant", conf_tensor(conf))
|
| 292 |
+
del w, qdata, scales
|
| 293 |
+
if quantized % 32 == 0:
|
| 294 |
+
torch.cuda.empty_cache()
|
| 295 |
+
quantized += 1
|
| 296 |
+
return quantized
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _slice_dtype(slice_):
|
| 300 |
+
return {"F32": torch.float32, "F16": torch.float16, "BF16": torch.bfloat16,
|
| 301 |
+
"I8": torch.int8, "U8": torch.uint8, "I64": torch.int64,
|
| 302 |
+
"F8_E4M3": torch.float8_e4m3fn}[slice_.get_dtype()]
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def _scale_shape(algo: str, out_features: int) -> list[int]:
|
| 306 |
+
"""Shape of `weight_scale`, as the layout actually returns it, without quantizing.
|
| 307 |
+
|
| 308 |
+
Both algos carry one scale per output channel; they disagree only on the rank.
|
| 309 |
+
`convrot_w4a4` hands back a 1-D `[out_features]`, and its dequantize kernel constrains
|
| 310 |
+
the argument to exactly 1-D. `int8_tensorwise` hands back `[out_features, 1]`.
|
| 311 |
+
|
| 312 |
+
Hardcoded because the plan pass must not quantize anything -- that is what makes
|
| 313 |
+
`--dry-run` free and what keeps the two-pass writer honest. The risk of hardcoding is
|
| 314 |
+
that it drifts from what the layout does; `StreamingWriter.write` closes that by
|
| 315 |
+
comparing this against the real tensor, so a wrong entry here is a loud failure on the
|
| 316 |
+
first write rather than a quietly mis-headered checkpoint.
|
| 317 |
+
"""
|
| 318 |
+
if algo == "convrot_w4a4":
|
| 319 |
+
return [out_features]
|
| 320 |
+
if algo == "int8_tensorwise":
|
| 321 |
+
return [out_features, 1]
|
| 322 |
+
raise ValueError("no known weight_scale shape for {!r}".format(algo))
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def _conf_len(algo: str, groupsize: int) -> int:
|
| 326 |
+
"""Byte length of the comfy_quant marker, without quantizing a weight to find out."""
|
| 327 |
+
if algo == "convrot_w4a4":
|
| 328 |
+
conf = {"format": "convrot_w4a4", "convrot_groupsize": groupsize,
|
| 329 |
+
"linear_dtype": "int4"}
|
| 330 |
+
else:
|
| 331 |
+
conf = {"format": "int8_tensorwise", "convrot": True, "convrot_groupsize": groupsize}
|
| 332 |
+
return len(conf_tensor(conf))
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def build_all_in_one_checkpoint(dit_path: str, text_encoder_path: str, vae_path: str,
|
| 336 |
+
out_path: str, fmt_name: str = "svdq", te_format: str = "w4a4",
|
| 337 |
+
rank: int = 64, variant: str = "unknown", groupsize: int = 256,
|
| 338 |
+
device: str = "cuda", dry_run: bool = False,
|
| 339 |
+
progress_cb=None) -> str:
|
| 340 |
+
"""Combine DiT, quantized text encoder, and VAE into a single checkpoint."""
|
| 341 |
+
fmt, rank = resolve_format(fmt_name, rank, rank_was_set=False)
|
| 342 |
+
|
| 343 |
+
with safe_open(dit_path, framework="pt", device="cpu") as handle:
|
| 344 |
+
dit_keys = list(handle.keys())
|
| 345 |
+
dit_prefix = detect_prefix(dit_keys, default="")
|
| 346 |
+
already = any(k.endswith(".comfy_quant") for k in dit_keys)
|
| 347 |
+
|
| 348 |
+
if not already and not dry_run:
|
| 349 |
+
raise RuntimeError(
|
| 350 |
+
"{} is a BF16 source. Quantize it first with quantize_krea2.py (or the "
|
| 351 |
+
"Quantize node) and pass the result as dit_path.".format(dit_path))
|
| 352 |
+
|
| 353 |
+
metadata = {
|
| 354 |
+
"krea2_svdquant_tool_version": __version__,
|
| 355 |
+
"krea2_allinone": "1",
|
| 356 |
+
"krea2_allinone_dit": os.path.basename(dit_path),
|
| 357 |
+
"krea2_allinone_te": os.path.basename(text_encoder_path),
|
| 358 |
+
"krea2_allinone_te_format": te_format,
|
| 359 |
+
"krea2_allinone_vae": os.path.basename(vae_path),
|
| 360 |
+
"krea2_svdquant_variant": variant,
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
writer = StreamingWriter(out_path, metadata)
|
| 364 |
+
with safe_open(dit_path, framework="pt", device="cpu") as dh:
|
| 365 |
+
for out_key, src in plan_dit(dh, dit_keys, dit_prefix):
|
| 366 |
+
s = dh.get_slice(src)
|
| 367 |
+
writer.plan(out_key, _slice_dtype(s), s.get_shape())
|
| 368 |
+
dit_planned = len(writer.entries)
|
| 369 |
+
|
| 370 |
+
with safe_open(vae_path, framework="pt", device="cpu") as vh:
|
| 371 |
+
for out_key, src in plan_vae(list(vh.keys())):
|
| 372 |
+
s = vh.get_slice(src)
|
| 373 |
+
writer.plan(out_key, _slice_dtype(s), s.get_shape())
|
| 374 |
+
vae_planned = len(writer.entries) - dit_planned
|
| 375 |
+
|
| 376 |
+
te_quantized = quantize_text_encoder(text_encoder_path, te_format, groupsize,
|
| 377 |
+
device, writer, dry_run=True)
|
| 378 |
+
te_planned = len(writer.entries) - dit_planned - vae_planned
|
| 379 |
+
|
| 380 |
+
if te_quantized != TE_EXPECTED:
|
| 381 |
+
raise RuntimeError(
|
| 382 |
+
"expected {} quantizable text-encoder linears ({} layers x {}), matched {}. "
|
| 383 |
+
"Layer naming in {} is not recognized.".format(
|
| 384 |
+
TE_EXPECTED, TE_LAYERS, len(TE_QUANT_SUFFIXES), te_quantized, text_encoder_path))
|
| 385 |
+
|
| 386 |
+
total = sum(e[3] for e in writer.entries)
|
| 387 |
+
by = lambda pred: sum(e[3] for e in writer.entries if pred(e[0])) / 1024 ** 3
|
| 388 |
+
|
| 389 |
+
stats = (
|
| 390 |
+
"planned {} tensors, {:.2f} GiB (diffusion: {:.2f} GiB, encoder: {:.2f} GiB [{} linears -> {}], vae: {:.2f} GiB)"
|
| 391 |
+
.format(len(writer.entries), total / 1024 ** 3, by(lambda k: k.startswith(DIT_PREFIX)),
|
| 392 |
+
by(lambda k: k.startswith(TE_PREFIX)), te_quantized, te_format,
|
| 393 |
+
by(lambda k: k.startswith(VAE_PREFIX)))
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
if dry_run:
|
| 397 |
+
return stats + "\n[dry-run: nothing written, GPU untouched]"
|
| 398 |
+
|
| 399 |
+
t0 = time.time()
|
| 400 |
+
total_steps = len(writer.entries)
|
| 401 |
+
step = 0
|
| 402 |
+
|
| 403 |
+
with writer as w:
|
| 404 |
+
with safe_open(dit_path, framework="pt", device="cpu") as dh:
|
| 405 |
+
for out_key, src in plan_dit(dh, dit_keys, dit_prefix):
|
| 406 |
+
w.write(out_key, dh.get_tensor(src))
|
| 407 |
+
step += 1
|
| 408 |
+
if progress_cb:
|
| 409 |
+
progress_cb(step, total_steps, "Copying DiT")
|
| 410 |
+
with safe_open(vae_path, framework="pt", device="cpu") as vh:
|
| 411 |
+
for out_key, src in plan_vae(list(vh.keys())):
|
| 412 |
+
w.write(out_key, vh.get_tensor(src))
|
| 413 |
+
step += 1
|
| 414 |
+
if progress_cb:
|
| 415 |
+
progress_cb(step, total_steps, "Copying VAE")
|
| 416 |
+
quantize_text_encoder(text_encoder_path, te_format, groupsize, device,
|
| 417 |
+
w, dry_run=False)
|
| 418 |
+
if progress_cb:
|
| 419 |
+
progress_cb(total_steps, total_steps, "Complete")
|
| 420 |
+
|
| 421 |
+
elapsed = time.time() - t0
|
| 422 |
+
final_size = os.path.getsize(out_path) / 1024 ** 3
|
| 423 |
+
summary = (
|
| 424 |
+
"baked all-in-one checkpoint: {} ({:.2f} GiB, {:.0f}s)\n"
|
| 425 |
+
" diffusion : {} (format {})\n"
|
| 426 |
+
" encoder : {} (quantized to {})\n"
|
| 427 |
+
" vae : {}\n"
|
| 428 |
+
"{}".format(
|
| 429 |
+
os.path.basename(out_path), final_size, elapsed,
|
| 430 |
+
os.path.basename(dit_path), fmt_name,
|
| 431 |
+
os.path.basename(text_encoder_path), te_format,
|
| 432 |
+
os.path.basename(vae_path), stats
|
| 433 |
+
)
|
| 434 |
+
)
|
| 435 |
+
return summary
|
quantize_krea2.py
CHANGED
|
@@ -1,844 +1,923 @@
|
|
| 1 |
-
"""Quantize a BF16 Krea2 checkpoint into a ComfyUI-native quantized checkpoint.
|
| 2 |
-
|
| 3 |
-
ComfyUI already ships native kernels for these formats via comfy_kitchen, so the output
|
| 4 |
-
loads with a plain ``UNETLoader`` -- no custom node, and ordinary LoRA loaders work.
|
| 5 |
-
|
| 6 |
-
Four targets:
|
| 7 |
-
|
| 8 |
-
* ``int8`` -> ``int8_tensorwise`` with per-channel scales + convrot (W8A8).
|
| 9 |
-
Natively accelerated on Ampere INT8 tensor cores.
|
| 10 |
-
* ``w4a4`` -> ``convrot_w4a4`` (W4A4), smaller and potentially faster, 4-bit quality.
|
| 11 |
-
* ``svdq`` -> the same W4A4 residual plus an SVDQuant low-rank bf16 branch, which
|
| 12 |
-
absorbs the outlier directions 4 bits handle worst. Needs this repo's
|
| 13 |
-
loader node; a plain ``UNETLoader`` cannot see the branch.
|
| 14 |
-
* ``fp8`` -> ``float8_e4m3fn``, a single per-tensor scale, no convrot, no calibration.
|
| 15 |
-
Lightest touch of the four -- best fidelity, smallest speedup, and the
|
| 16 |
-
only one that needs no rotation or outlier handling because 8-bit float
|
| 17 |
-
already has enough dynamic range for these weights.
|
| 18 |
-
|
| 19 |
-
None of them need a calibration dataset: int8/w4a4/svdq spread outliers analytically via
|
| 20 |
-
the convrot (group-wise Hadamard) rotation, and activations are quantized by the kernel at
|
| 21 |
-
run time; fp8 just needs one abs-max scale per tensor.
|
| 22 |
-
|
| 23 |
-
Only the 224 transformer-block linears are quantized, matching the layer set used by the
|
| 24 |
-
reference ``krea2_raw_int8_convrot`` checkpoint. Norms, modulation, the text-fusion stack
|
| 25 |
-
and the final layer stay in their original precision -- they are small and quantization
|
| 26 |
-
noise there is disproportionately visible.
|
| 27 |
-
|
| 28 |
-
The layer selection is variant-agnostic: Krea 2 Turbo and the base (non-distilled)
|
| 29 |
-
release share the same block naming, so the same command converts either. What differs is
|
| 30 |
-
how you sample afterwards, not how you quantize -- pass ``--variant`` so the output is
|
| 31 |
-
named accordingly and the file records which one it came from.
|
| 32 |
-
|
| 33 |
-
python quantize_krea2.py <bf16-model.safetensors> [--format int8|w4a4|svdq|fp8]
|
| 34 |
-
[--rank 64] [--variant turbo|base] [--out PATH]
|
| 35 |
-
"""
|
| 36 |
-
|
| 37 |
-
from __future__ import annotations
|
| 38 |
-
|
| 39 |
-
import argparse
|
| 40 |
-
import gc
|
| 41 |
-
import importlib.util
|
| 42 |
-
import json
|
| 43 |
-
import math
|
| 44 |
-
import os
|
| 45 |
-
import sys
|
| 46 |
-
import time
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
from safetensors
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
#
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
#
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
"""
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
#
|
| 199 |
-
#
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
#
|
| 203 |
-
#
|
| 204 |
-
#
|
| 205 |
-
#
|
| 206 |
-
#
|
| 207 |
-
#
|
| 208 |
-
#
|
| 209 |
-
#
|
| 210 |
-
#
|
| 211 |
-
#
|
| 212 |
-
#
|
| 213 |
-
#
|
| 214 |
-
#
|
| 215 |
-
#
|
| 216 |
-
#
|
| 217 |
-
#
|
| 218 |
-
#
|
| 219 |
-
#
|
| 220 |
-
#
|
| 221 |
-
#
|
| 222 |
-
#
|
| 223 |
-
#
|
| 224 |
-
#
|
| 225 |
-
#
|
| 226 |
-
#
|
| 227 |
-
#
|
| 228 |
-
#
|
| 229 |
-
#
|
| 230 |
-
#
|
| 231 |
-
#
|
| 232 |
-
#
|
| 233 |
-
#
|
| 234 |
-
#
|
| 235 |
-
#
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
""
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
""
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
if
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
"""
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
(
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
else
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
if
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
"""
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
#
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Quantize a BF16 Krea2 checkpoint into a ComfyUI-native quantized checkpoint.
|
| 2 |
+
|
| 3 |
+
ComfyUI already ships native kernels for these formats via comfy_kitchen, so the output
|
| 4 |
+
loads with a plain ``UNETLoader`` -- no custom node, and ordinary LoRA loaders work.
|
| 5 |
+
|
| 6 |
+
Four targets:
|
| 7 |
+
|
| 8 |
+
* ``int8`` -> ``int8_tensorwise`` with per-channel scales + convrot (W8A8).
|
| 9 |
+
Natively accelerated on Ampere INT8 tensor cores.
|
| 10 |
+
* ``w4a4`` -> ``convrot_w4a4`` (W4A4), smaller and potentially faster, 4-bit quality.
|
| 11 |
+
* ``svdq`` -> the same W4A4 residual plus an SVDQuant low-rank bf16 branch, which
|
| 12 |
+
absorbs the outlier directions 4 bits handle worst. Needs this repo's
|
| 13 |
+
loader node; a plain ``UNETLoader`` cannot see the branch.
|
| 14 |
+
* ``fp8`` -> ``float8_e4m3fn``, a single per-tensor scale, no convrot, no calibration.
|
| 15 |
+
Lightest touch of the four -- best fidelity, smallest speedup, and the
|
| 16 |
+
only one that needs no rotation or outlier handling because 8-bit float
|
| 17 |
+
already has enough dynamic range for these weights.
|
| 18 |
+
|
| 19 |
+
None of them need a calibration dataset: int8/w4a4/svdq spread outliers analytically via
|
| 20 |
+
the convrot (group-wise Hadamard) rotation, and activations are quantized by the kernel at
|
| 21 |
+
run time; fp8 just needs one abs-max scale per tensor.
|
| 22 |
+
|
| 23 |
+
Only the 224 transformer-block linears are quantized, matching the layer set used by the
|
| 24 |
+
reference ``krea2_raw_int8_convrot`` checkpoint. Norms, modulation, the text-fusion stack
|
| 25 |
+
and the final layer stay in their original precision -- they are small and quantization
|
| 26 |
+
noise there is disproportionately visible.
|
| 27 |
+
|
| 28 |
+
The layer selection is variant-agnostic: Krea 2 Turbo and the base (non-distilled)
|
| 29 |
+
release share the same block naming, so the same command converts either. What differs is
|
| 30 |
+
how you sample afterwards, not how you quantize -- pass ``--variant`` so the output is
|
| 31 |
+
named accordingly and the file records which one it came from.
|
| 32 |
+
|
| 33 |
+
python quantize_krea2.py <bf16-model.safetensors> [--format int8|w4a4|svdq|fp8]
|
| 34 |
+
[--rank 64] [--variant turbo|base] [--out PATH]
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
from __future__ import annotations
|
| 38 |
+
|
| 39 |
+
import argparse
|
| 40 |
+
import gc
|
| 41 |
+
import importlib.util
|
| 42 |
+
import json
|
| 43 |
+
import math
|
| 44 |
+
import os
|
| 45 |
+
import sys
|
| 46 |
+
import time
|
| 47 |
+
import zlib
|
| 48 |
+
|
| 49 |
+
__version__ = "0.2.0"
|
| 50 |
+
|
| 51 |
+
import torch
|
| 52 |
+
from safetensors import safe_open
|
| 53 |
+
from safetensors.torch import save_file
|
| 54 |
+
|
| 55 |
+
def _find_comfyui_root() -> str | None:
|
| 56 |
+
"""Locate the ComfyUI root so `comfy.*` imports work regardless of cwd.
|
| 57 |
+
|
| 58 |
+
Preference order: $COMFYUI_PATH, then walking up from this file (this script
|
| 59 |
+
lives in ComfyUI/custom_nodes/<pkg>/, so comfy/ is two levels up). None if
|
| 60 |
+
neither resolves -- deliberately *not* cwd. This module is imported by the
|
| 61 |
+
Quantize node inside ComfyUI's own process, and putting cwd at sys.path[0]
|
| 62 |
+
there lets any file in the working directory shadow a real import.
|
| 63 |
+
"""
|
| 64 |
+
env = os.environ.get("COMFYUI_PATH")
|
| 65 |
+
if env and os.path.isdir(os.path.join(env, "comfy")):
|
| 66 |
+
return env
|
| 67 |
+
here = os.path.dirname(os.path.abspath(__file__))
|
| 68 |
+
candidate = os.path.abspath(os.path.join(here, "..", ".."))
|
| 69 |
+
if os.path.isdir(os.path.join(candidate, "comfy")):
|
| 70 |
+
return candidate
|
| 71 |
+
for extra in ("D:/ComfyUI", "C:/ComfyUI", os.path.abspath(os.path.join(here, "..", "ComfyUI"))):
|
| 72 |
+
if os.path.isdir(os.path.join(extra, "comfy")):
|
| 73 |
+
return extra
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# Running inside ComfyUI, `comfy` already imports and the path needs no help.
|
| 78 |
+
if importlib.util.find_spec("comfy") is None:
|
| 79 |
+
_root = _find_comfyui_root()
|
| 80 |
+
if _root is not None:
|
| 81 |
+
sys.path.insert(0, _root)
|
| 82 |
+
|
| 83 |
+
# Guarded because __init__.py imports this module transitively: an unguarded failure
|
| 84 |
+
# here would deregister every node in the pack, including Env Check, whose whole job is
|
| 85 |
+
# diagnosing a broken quantization backend. Degrade to "no formats available" instead.
|
| 86 |
+
QUANT_UNAVAILABLE = ""
|
| 87 |
+
try:
|
| 88 |
+
from comfy.quant_ops import QUANT_ALGOS, get_layout_class # noqa: E402
|
| 89 |
+
except Exception as exc: # pragma: no cover - depends on the ComfyUI build
|
| 90 |
+
QUANT_ALGOS = {}
|
| 91 |
+
get_layout_class = None
|
| 92 |
+
QUANT_UNAVAILABLE = "{}: {}".format(type(exc).__name__, exc)
|
| 93 |
+
|
| 94 |
+
# Relative improvement the refinement loop must keep making to earn another iteration. See
|
| 95 |
+
# `svdquant_split` for the traced numbers this default comes from.
|
| 96 |
+
REFINE_TOL = 0.001
|
| 97 |
+
|
| 98 |
+
# Deterministic by default. The low-rank split is a *randomized* SVD, so an unseeded build is
|
| 99 |
+
# irreproducible even on the same machine -- two files that differ by ~1e-4 per weight and by
|
| 100 |
+
# a visibly different image after 8 steps of sampling. `None` restores the old ambient-RNG
|
| 101 |
+
# behaviour for anyone who wants to sample the spread rather than pin it.
|
| 102 |
+
DEFAULT_SEED = 0
|
| 103 |
+
|
| 104 |
+
_QUANT_SUFFIXES = ("attn.wq", "attn.wk", "attn.wv", "attn.gate", "attn.wo",
|
| 105 |
+
"mlp.gate", "mlp.up", "mlp.down")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# Checkpoints ship either bare ("blocks.0...") or prefixed ("model.diffusion_model.blocks.0...").
|
| 109 |
+
LAYER_PREFIXES = ("model.diffusion_model.", "diffusion_model.", "")
|
| 110 |
+
|
| 111 |
+
# Formats we know how to reconstruct back to BF16 from disk alone. FP8 storage is one
|
| 112 |
+
# byte per element with no packing or pre-rotation, so `qdata * scale` recovers the
|
| 113 |
+
# original tensor exactly as it was cast -- no architecture knowledge needed beyond
|
| 114 |
+
# what's already in the file. INT8/W4A4 sources pack multiple values per byte and are
|
| 115 |
+
# rotated (convrot) before quantization; unpacking those correctly needs the exact
|
| 116 |
+
# in/out feature counts from the live nn.Linear, which isn't recoverable from the
|
| 117 |
+
# checkpoint alone, so those are rejected instead.
|
| 118 |
+
_DEQUANTIZABLE_FORMATS = ("float8_e4m3fn", "float8_e5m2")
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def detect_prefix(keys, default: str | None = None) -> str:
|
| 122 |
+
"""Return the prefix the transformer blocks live under.
|
| 123 |
+
|
| 124 |
+
Raises `RuntimeError` if no prefix matches and no `default` was given. Not
|
| 125 |
+
`SystemExit`: that is a `BaseException`, and `convert()` runs inside ComfyUI's
|
| 126 |
+
executor via the Quantize node, where a BaseException escapes error handling
|
| 127 |
+
entirely. `main()` catches RuntimeError and prints it as a clean CLI line.
|
| 128 |
+
"""
|
| 129 |
+
for prefix in LAYER_PREFIXES:
|
| 130 |
+
if any(k.startswith("{}blocks.".format(prefix)) for k in keys):
|
| 131 |
+
return prefix
|
| 132 |
+
if default is not None:
|
| 133 |
+
return default
|
| 134 |
+
raise RuntimeError(
|
| 135 |
+
"Could not find transformer blocks in this checkpoint. Expected keys like\n"
|
| 136 |
+
" blocks.0.attn.wq.weight or model.diffusion_model.blocks.0.attn.wq.weight\n"
|
| 137 |
+
"This does not look like a Krea 2 diffusion model."
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def check_requantizable(handle, keys, prefix: str) -> None:
|
| 142 |
+
"""Make sure every target layer that's already quantized is something we can
|
| 143 |
+
dequantize back to BF16 (see `_DEQUANTIZABLE_FORMATS`). Anything else -- most
|
| 144 |
+
importantly INT8/W4A4 -- needs the original BF16 (or FP16) release instead.
|
| 145 |
+
"""
|
| 146 |
+
for key in keys:
|
| 147 |
+
if not key.endswith(".comfy_quant"):
|
| 148 |
+
continue
|
| 149 |
+
layer = key[: -len(".comfy_quant")]
|
| 150 |
+
if not is_target(layer, prefix):
|
| 151 |
+
continue
|
| 152 |
+
conf = json.loads(bytes(handle.get_tensor(key).tolist()))
|
| 153 |
+
fmt = conf.get("format")
|
| 154 |
+
if fmt not in _DEQUANTIZABLE_FORMATS:
|
| 155 |
+
raise RuntimeError(
|
| 156 |
+
"Layer {} is already quantized as '{}'. Only FP8-quantized layers can be "
|
| 157 |
+
"automatically reconstructed and re-quantized; for INT8/W4A4 sources, use "
|
| 158 |
+
"the original BF16 (or FP16) release of the model instead.".format(layer, fmt)
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def dequantize_target_weight(handle, layer: str, device: str) -> torch.Tensor:
|
| 163 |
+
"""Load a target layer's weight as BF16, dequantizing it first if it's FP8.
|
| 164 |
+
|
| 165 |
+
Scaled FP8 (has a `comfy_quant` marker + `weight_scale`): ``qdata.float() * scale``.
|
| 166 |
+
Unscaled FP8 (a bare ``.to(float8_e4m3fn)`` cast, no marker): ``qdata`` as-is.
|
| 167 |
+
Anything else is already ruled out by `check_requantizable`.
|
| 168 |
+
"""
|
| 169 |
+
weight = handle.get_tensor("{}.weight".format(layer)).to(device=device)
|
| 170 |
+
conf_key = "{}.comfy_quant".format(layer)
|
| 171 |
+
if conf_key in handle.keys():
|
| 172 |
+
scale = handle.get_tensor("{}.weight_scale".format(layer)).to(device=device).float()
|
| 173 |
+
return (weight.to(torch.float32) * scale).to(torch.bfloat16)
|
| 174 |
+
return weight.to(torch.bfloat16)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def is_target(layer: str, prefix: str) -> bool:
|
| 178 |
+
"""Only the transformer blocks; txtfusion and friends stay high precision."""
|
| 179 |
+
if not layer.startswith("{}blocks.".format(prefix)):
|
| 180 |
+
return False
|
| 181 |
+
return any(layer.endswith(s) for s in _QUANT_SUFFIXES)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def leaf_name(layer: str, prefix: str) -> str | None:
|
| 185 |
+
"""``blocks.7.attn.wq`` -> ``attn.wq``. None if the layer is not inside a block.
|
| 186 |
+
|
| 187 |
+
Only used to make a "nothing matched" failure self-diagnosing: a Krea 2 variant that
|
| 188 |
+
keeps the ``blocks.`` container but renames its leaves would otherwise quantize zero
|
| 189 |
+
layers and still write a full-size file.
|
| 190 |
+
"""
|
| 191 |
+
head = "{}blocks.".format(prefix)
|
| 192 |
+
if not layer.startswith(head):
|
| 193 |
+
return None
|
| 194 |
+
rest = layer[len(head):]
|
| 195 |
+
return rest.split(".", 1)[1] if "." in rest else None
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
# Rank is bought in multiples of this, so a reallocated budget lands on the same grid the
|
| 199 |
+
# `rank` widget and the CLI already use.
|
| 200 |
+
_RANK_STEP = 8
|
| 201 |
+
|
| 202 |
+
# Per-leaf rank multipliers, relative to a uniform budget.
|
| 203 |
+
#
|
| 204 |
+
# Uniform rank is the obvious default and the wrong one. Measured on Krea2 Turbo at rank 64,
|
| 205 |
+
# error removed per million branch parameters spans 6.9x across the eight leaves:
|
| 206 |
+
#
|
| 207 |
+
# attn.wk 0.0992 (1536x6144, branch 491K params, absorbs 30.1% of the error)
|
| 208 |
+
# attn.wv 0.0844 (1536x6144, 491K, 25.6%)
|
| 209 |
+
# attn.wq 0.0445 (6144x6144, 786K, 21.6%)
|
| 210 |
+
# attn.wo 0.0411 (6144x6144, 786K, 17.6%)
|
| 211 |
+
# attn.gate 0.0384 (6144x6144, 786K, 18.7%)
|
| 212 |
+
# mlp.down 0.0169 (6144x16384, 1.44M, 14.3%)
|
| 213 |
+
# mlp.gate 0.0155 (16384x6144, 1.44M, 13.8%)
|
| 214 |
+
# mlp.up 0.0143 (16384x6144, 1.44M, 12.7%)
|
| 215 |
+
#
|
| 216 |
+
# The cause is GQA: Krea 2 has 12 kv heads against 48 query heads, so wk/wv are 1536-wide and
|
| 217 |
+
# their branch costs a third of an MLP branch -- while absorbing twice as much error. Uniform
|
| 218 |
+
# rank therefore spends most of the budget on the layers that reward it least.
|
| 219 |
+
#
|
| 220 |
+
# These multipliers come from a greedy allocation under a fixed byte budget (branch cost is
|
| 221 |
+
# exactly linear in rank, so scaling all of them preserves byte-neutrality at any base rank).
|
| 222 |
+
# At base rank 64 they reproduce the solved table: wk 360, wv 256, wq 72, wo 64, gate 56,
|
| 223 |
+
# mlp 8, and the checkpoint comes out 0.02% smaller than uniform rank 64.
|
| 224 |
+
#
|
| 225 |
+
# MEASURED, and it does not deliver. The greedy solve predicted 6% less mean layer weight
|
| 226 |
+
# error; the images say otherwise. 10 prompts, seed 987654321, against the same BF16 reference:
|
| 227 |
+
#
|
| 228 |
+
# uniform rank 64 LPIPS 0.3403 +-0.1019
|
| 229 |
+
# gqa rank 64 LPIPS 0.3523 +-0.0473
|
| 230 |
+
#
|
| 231 |
+
# gqa is closer on 5 of 10 prompts, paired t = +0.55 on 9 df. No mean effect in either
|
| 232 |
+
# direction. Two things did not survive scrutiny and are recorded so nobody re-derives them:
|
| 233 |
+
# the -6% rests on an absorption ~ sqrt(rank) model fitted to nothing stronger than "returns
|
| 234 |
+
# are sublinear", and the tempting pattern that gqa wins on the hard prompts is an artifact --
|
| 235 |
+
# re-pairing the gqa scores at random reproduces the same -0.92 correlation, because the
|
| 236 |
+
# baseline appears on both sides of the difference.
|
| 237 |
+
#
|
| 238 |
+
# What is left is real but narrow: the spread across prompts shrinks (variance ratio 4.64,
|
| 239 |
+
# F-test p = 0.032) and the worst prompt improves, 0.4975 -> 0.4470. That is one nominally
|
| 240 |
+
# significant result among several things tried on the same 10 images, so it is a lead for a
|
| 241 |
+
# larger measurement, not a reason to change the default. `uniform` stays the default.
|
| 242 |
+
#
|
| 243 |
+
# The 6.9x spread itself is solid -- it is measured weight error over real shapes. The lesson
|
| 244 |
+
# is that weight error is a poor proxy for image outcome, which is now the third time this
|
| 245 |
+
# has held on this model (the refinement objective and the depth hypothesis were the others).
|
| 246 |
+
_GQA_ALLOC = {
|
| 247 |
+
"attn.wk": 5.625, "attn.wv": 4.0, "attn.wq": 1.125, "attn.wo": 1.0, "attn.gate": 0.875,
|
| 248 |
+
"mlp.gate": 0.125, "mlp.up": 0.125, "mlp.down": 0.125,
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
RANK_ALLOCATIONS: dict[str, dict[str, float] | None] = {
|
| 252 |
+
"uniform": None,
|
| 253 |
+
"gqa": _GQA_ALLOC,
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def leaf_ranks(rank: int, rank_alloc: str = "uniform") -> dict[str, int]:
|
| 258 |
+
"""Expand a rank budget into a per-leaf rank, one entry per name in `_QUANT_SUFFIXES`.
|
| 259 |
+
|
| 260 |
+
`uniform` gives every leaf the same rank -- the historical behaviour. Other allocations
|
| 261 |
+
redistribute the same total branch bytes according to `RANK_ALLOCATIONS`.
|
| 262 |
+
"""
|
| 263 |
+
if rank_alloc not in RANK_ALLOCATIONS:
|
| 264 |
+
raise RuntimeError("unknown rank allocation {!r}; expected one of {}".format(
|
| 265 |
+
rank_alloc, ", ".join(sorted(RANK_ALLOCATIONS))))
|
| 266 |
+
mults = RANK_ALLOCATIONS[rank_alloc]
|
| 267 |
+
if mults is None:
|
| 268 |
+
return {leaf: rank for leaf in _QUANT_SUFFIXES}
|
| 269 |
+
|
| 270 |
+
# Byte-neutrality is the whole point of a non-uniform allocation, and it only survives
|
| 271 |
+
# while every leaf can be expressed on the step-8 grid. Below that the smallest leaves get
|
| 272 |
+
# rounded *up* to the floor and the file grows: at budget 16 the MLP leaves want rank 2,
|
| 273 |
+
# take 8, and the checkpoint comes out 20% larger than uniform -- which would make any
|
| 274 |
+
# comparison against uniform meaningless. Refuse rather than quietly change the size.
|
| 275 |
+
smallest = min(mults.values())
|
| 276 |
+
minimum = int(math.ceil(_RANK_STEP / smallest / _RANK_STEP)) * _RANK_STEP
|
| 277 |
+
if rank < minimum:
|
| 278 |
+
raise RuntimeError(
|
| 279 |
+
"rank allocation {!r} needs a budget of at least {} (you asked for {}). Its "
|
| 280 |
+
"smallest leaf gets {}x the budget, which below that rounds up to the step-{} "
|
| 281 |
+
"floor and makes the checkpoint *larger* than uniform instead of the same size."
|
| 282 |
+
.format(rank_alloc, minimum, rank, smallest, _RANK_STEP))
|
| 283 |
+
|
| 284 |
+
out = {}
|
| 285 |
+
for leaf in _QUANT_SUFFIXES:
|
| 286 |
+
out[leaf] = int(round(rank * mults[leaf] / _RANK_STEP)) * _RANK_STEP
|
| 287 |
+
return out
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def rank_for_leaf(ranks: dict[str, int], leaf: str | None) -> int:
|
| 291 |
+
"""The branch rank for a target layer, given its leaf name. 0 means "no branch".
|
| 292 |
+
|
| 293 |
+
Matches by suffix because `is_target` does. An exact-key lookup would disagree with it
|
| 294 |
+
on any variant that nested the leaves one level deeper (`blocks.0.wrapper.attn.wq`):
|
| 295 |
+
such a layer is a quantization target, so it would be quantized, but would find no rank
|
| 296 |
+
entry and be written without a low-rank branch -- an svdq checkpoint silently missing
|
| 297 |
+
branches on part of the network, which is the class of quiet wrongness the zero-layer
|
| 298 |
+
check below exists to prevent.
|
| 299 |
+
"""
|
| 300 |
+
if not ranks or leaf is None:
|
| 301 |
+
return 0
|
| 302 |
+
if leaf in ranks:
|
| 303 |
+
return ranks[leaf]
|
| 304 |
+
for suffix, value in ranks.items():
|
| 305 |
+
if leaf.endswith(suffix):
|
| 306 |
+
return value
|
| 307 |
+
return 0
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def svd_lowrank(weight: torch.Tensor, rank: int, oversample: int = 16, niter: int = 2):
|
| 311 |
+
"""Randomized truncated SVD: return (L1 [out, rank], L2 [rank, in]) with W ~ L1 @ L2.
|
| 312 |
+
|
| 313 |
+
Randomized rather than exact because these are 6144x16384 matrices and only the top
|
| 314 |
+
few dozen singular directions matter here; `oversample` extra probe columns plus a
|
| 315 |
+
couple of power iterations recover them to well past the precision 4-bit quantization
|
| 316 |
+
cares about.
|
| 317 |
+
|
| 318 |
+
"Randomized" is load-bearing for reproducibility: `torch.svd_lowrank` draws its probe
|
| 319 |
+
matrix from the *ambient* RNG, so seeding is the caller's job (`svdquant_split` does it
|
| 320 |
+
per layer). Left unseeded, two runs of this script on the same GPU with identical
|
| 321 |
+
arguments produce different checkpoints -- close, but not equal, and visibly so once a
|
| 322 |
+
sampler amplifies them.
|
| 323 |
+
"""
|
| 324 |
+
w = weight.float()
|
| 325 |
+
min_dim = min(w.shape)
|
| 326 |
+
rank = max(1, min(int(rank), min_dim))
|
| 327 |
+
q = min(rank + max(0, int(oversample)), min_dim)
|
| 328 |
+
|
| 329 |
+
if min_dim <= max(q, 32):
|
| 330 |
+
u, s, vh = torch.linalg.svd(w, full_matrices=False)
|
| 331 |
+
u_r, s_r, vh_r = u[:, :rank], s[:rank], vh[:rank, :]
|
| 332 |
+
else:
|
| 333 |
+
u, s, v = torch.svd_lowrank(w, q=q, niter=max(0, int(niter)))
|
| 334 |
+
u_r, s_r, vh_r = u[:, :rank], s[:rank], v[:, :rank].transpose(-2, -1)
|
| 335 |
+
|
| 336 |
+
return (u_r * s_r.unsqueeze(0)).contiguous(), vh_r.contiguous()
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def _act_weighting(act_rms: torch.Tensor | None, in_features: int, device, floor: float = 0.05):
|
| 340 |
+
"""Turn a captured activation RMS vector into the column weighting `d`, or None.
|
| 341 |
+
|
| 342 |
+
Normalised to mean 1 so the weighting only redistributes emphasis and never rescales the
|
| 343 |
+
problem -- otherwise the error metric would no longer be comparable across layers and the
|
| 344 |
+
refinement loop's stopping threshold would mean something different in each one.
|
| 345 |
+
|
| 346 |
+
The floor matters more than it looks. `l2` is recovered by dividing by `d`, so a channel
|
| 347 |
+
whose activation is ~0 would divide a finite number by ~0 and put an enormous row into the
|
| 348 |
+
branch -- a branch that then dominates the output for a channel the model never actually
|
| 349 |
+
drives. Clamping to 5% of the mean bounds that amplification at 20x.
|
| 350 |
+
"""
|
| 351 |
+
if act_rms is None:
|
| 352 |
+
return None
|
| 353 |
+
d = act_rms.to(device=device, dtype=torch.float32).flatten()
|
| 354 |
+
if d.numel() != in_features:
|
| 355 |
+
raise RuntimeError("activation stats have {} channels but the weight has {}"
|
| 356 |
+
.format(d.numel(), in_features))
|
| 357 |
+
mean = d.mean()
|
| 358 |
+
if not torch.isfinite(mean) or mean <= 0:
|
| 359 |
+
return None
|
| 360 |
+
d = d / mean
|
| 361 |
+
return d.clamp(min=floor)
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def svdquant_split(weight: torch.Tensor, rank: int, fmt: str, groupsize: int,
|
| 365 |
+
refine_iters: int = 100, act_rms: torch.Tensor | None = None,
|
| 366 |
+
refine_tol: float = REFINE_TOL, seed: int | None = None):
|
| 367 |
+
"""SVDQuant ordering: pull a low-rank bf16 branch out of W, quantize the residual.
|
| 368 |
+
|
| 369 |
+
The low-rank branch absorbs the outlier-heavy directions, so the part that has to
|
| 370 |
+
survive 4 bits is better conditioned.
|
| 371 |
+
|
| 372 |
+
A single SVD of W is only the first guess: it picks the directions that are largest
|
| 373 |
+
in W, which is not the same as the directions the quantizer handles worst. So this
|
| 374 |
+
iterates the way deepcompressor does -- re-fit the branch to whatever the quantizer
|
| 375 |
+
is *currently* getting wrong, requantize, repeat -- and keeps the best result. Pass
|
| 376 |
+
``refine_iters=0`` for the plain single-shot split.
|
| 377 |
+
|
| 378 |
+
Iteration one is exactly that single-shot split and the best is kept, so refining
|
| 379 |
+
can only match or beat it. Measured on Krea2 Turbo, rank 64: ~10% less
|
| 380 |
+
reconstruction error, every layer improving.
|
| 381 |
+
|
| 382 |
+
`refine_tol` is what stops it, and it is *relative* for a reason. It used to be an
|
| 383 |
+
absolute `1e-6` against an error of order 1e-1, so the loop only stopped when an
|
| 384 |
+
iteration failed to improve by one part in a hundred thousand -- which measured at a mean
|
| 385 |
+
of **79.8 of the 100 iterations** across a sample of twelve layers at rank 256. What those
|
| 386 |
+
iterations buy, traced per iteration on the same layers:
|
| 387 |
+
|
| 388 |
+
stop at iterations reconstruction error
|
| 389 |
+
1e-6 absolute (old) ~80 baseline
|
| 390 |
+
0.5% relative ~9 +4.3%
|
| 391 |
+
0.1% relative (now) ~22 +2.0%
|
| 392 |
+
|
| 393 |
+
Conversion goes from ~14 minutes to ~4. The 2% is reconstruction error, which this repo
|
| 394 |
+
has three times measured to be a poor predictor of image outcome (see `--rank-alloc` in
|
| 395 |
+
the README), so read it as a conversion-time change rather than a quality one.
|
| 396 |
+
|
| 397 |
+
The objective is weight reconstruction error, which needs no calibration data.
|
| 398 |
+
That is the true output error under the assumption that the input covariance is
|
| 399 |
+
identity -- and spreading outliers with the convrot rotation is what makes that
|
| 400 |
+
assumption a reasonable one. Closing the remaining gap to deepcompressor means
|
| 401 |
+
measuring the real covariance, which is what makes their conversion take hours.
|
| 402 |
+
|
| 403 |
+
`seed` makes the run reproducible. The refinement draws a fresh random probe matrix on
|
| 404 |
+
every iteration through `svd_lowrank`, from the ambient RNG, so without this two builds
|
| 405 |
+
of the same checkpoint with the same arguments are merely *similar*: measured at ~1e-4
|
| 406 |
+
relative on the dequantized weights, which is small until a sampler spends 8-50 steps
|
| 407 |
+
compounding it and you get a visibly different image. Seeding once here rather than per
|
| 408 |
+
iteration is enough -- the whole sequence of draws then follows from one number. Pass
|
| 409 |
+
`None` for the old ambient-RNG behaviour.
|
| 410 |
+
|
| 411 |
+
Reproducibility is per device, not across devices: CPU and CUDA generate different
|
| 412 |
+
numbers from the same seed, and their GEMM reduction orders differ too, so a CPU build
|
| 413 |
+
and a GPU build of the same checkpoint will never be equal. `convert` records the device
|
| 414 |
+
in the metadata for that reason.
|
| 415 |
+
|
| 416 |
+
Returns None for a degenerate weight (all-zero, or one that makes the error metric
|
| 417 |
+
non-finite); the caller quantizes those without a branch rather than aborting.
|
| 418 |
+
"""
|
| 419 |
+
if seed is not None:
|
| 420 |
+
# fork_rng so a caller's RNG state survives: this runs inside somebody else's
|
| 421 |
+
# process (ComfyUI), where silently reseeding the global generator would make every
|
| 422 |
+
# later sampler node in the same session reproduce a different image.
|
| 423 |
+
with torch.random.fork_rng(devices=_rng_devices(weight.device)):
|
| 424 |
+
torch.manual_seed(seed)
|
| 425 |
+
return _svdquant_split(weight, rank, fmt, groupsize, refine_iters, act_rms,
|
| 426 |
+
refine_tol)
|
| 427 |
+
return _svdquant_split(weight, rank, fmt, groupsize, refine_iters, act_rms, refine_tol)
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
def _layer_seed(seed: int | None, layer: str) -> int | None:
|
| 431 |
+
"""A per-layer seed derived from the run's seed and the layer name.
|
| 432 |
+
|
| 433 |
+
Per layer rather than one seed for the whole run so that the result does not depend on
|
| 434 |
+
the order layers happen to be visited in, nor on how many refinement iterations the
|
| 435 |
+
previous layer took before its tolerance stopped it. Either would make a rebuild with a
|
| 436 |
+
different `--refine-tol` diverge everywhere instead of only where it should.
|
| 437 |
+
|
| 438 |
+
`zlib.crc32` rather than `hash()`: Python randomises string hashing per process, so
|
| 439 |
+
`hash()` here would be reproducible within a run and useless across runs -- the exact
|
| 440 |
+
failure this is meant to fix, hidden one level deeper.
|
| 441 |
+
"""
|
| 442 |
+
if seed is None:
|
| 443 |
+
return None
|
| 444 |
+
return (seed + zlib.crc32(layer.encode("utf-8"))) % (2 ** 63)
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def _rng_devices(device: torch.device) -> list:
|
| 448 |
+
"""Which generators `fork_rng` has to save and restore for work on `device`."""
|
| 449 |
+
return [device] if device.type == "cuda" else []
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def _svdquant_split(weight: torch.Tensor, rank: int, fmt: str, groupsize: int,
|
| 453 |
+
refine_iters: int, act_rms, refine_tol: float):
|
| 454 |
+
w = weight.float()
|
| 455 |
+
d = _act_weighting(act_rms, w.shape[1], w.device)
|
| 456 |
+
# With a weighting, both the SVD and the error metric work in the scaled space: minimising
|
| 457 |
+
# ||(W - W_hat) diag(d)|| approximates the error the layer's real inputs would produce,
|
| 458 |
+
# which is what actually reaches the image. Without one this is the identity and the whole
|
| 459 |
+
# function reduces to exactly the old behaviour.
|
| 460 |
+
w_norm = torch.linalg.matrix_norm(w if d is None else w * d).item()
|
| 461 |
+
if not math.isfinite(w_norm) or w_norm == 0.0:
|
| 462 |
+
return None
|
| 463 |
+
qw = torch.zeros((), device=w.device, dtype=torch.float32)
|
| 464 |
+
|
| 465 |
+
best = None
|
| 466 |
+
best_err = float("inf")
|
| 467 |
+
for _ in range(max(1, refine_iters)):
|
| 468 |
+
target = w - qw
|
| 469 |
+
if d is None:
|
| 470 |
+
l1, l2 = svd_lowrank(target, rank, oversample=16, niter=2)
|
| 471 |
+
else:
|
| 472 |
+
# Fit in the scaled space so the retained directions are the ones that matter
|
| 473 |
+
# once real activations hit them, then undo the scale on `l2` so `l1 @ l2` is
|
| 474 |
+
# still an approximation of the *unscaled* residual.
|
| 475 |
+
l1, l2s = svd_lowrank(target * d, rank, oversample=16, niter=2)
|
| 476 |
+
l2 = l2s / d
|
| 477 |
+
del l2s
|
| 478 |
+
l1 = l1.to(torch.bfloat16)
|
| 479 |
+
l2 = l2.to(torch.bfloat16)
|
| 480 |
+
lw = l1.float() @ l2.float()
|
| 481 |
+
# The quantizer must see the unscaled residual: the scale is an analysis device, and
|
| 482 |
+
# baking it into what gets written would change what the kernel reconstructs.
|
| 483 |
+
residual = (w - lw).to(torch.bfloat16)
|
| 484 |
+
|
| 485 |
+
qdata, params, layout, _ = _quantize_raw(residual, fmt, groupsize)
|
| 486 |
+
qw = layout.dequantize(qdata, params).float()
|
| 487 |
+
del qdata, params
|
| 488 |
+
|
| 489 |
+
left = w - (lw + qw)
|
| 490 |
+
err = (torch.linalg.matrix_norm(left if d is None else left * d) / w_norm).item()
|
| 491 |
+
del lw, left
|
| 492 |
+
# NaN fails every comparison, so without this the loop would neither break nor
|
| 493 |
+
# ever update `best` -- it would burn all `refine_iters` and return the last
|
| 494 |
+
# split rather than the best one.
|
| 495 |
+
if not math.isfinite(err):
|
| 496 |
+
break
|
| 497 |
+
if best is not None and err >= best_err * (1.0 - refine_tol):
|
| 498 |
+
break # refinement has stopped paying off
|
| 499 |
+
best_err, best = err, (residual, l1, l2)
|
| 500 |
+
|
| 501 |
+
return best
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
def _quantize_raw(weight: torch.Tensor, fmt: str, groupsize: int):
|
| 505 |
+
"""Quantize to `fmt`, handing back the layout and params too.
|
| 506 |
+
|
| 507 |
+
Split out from `quantize_weight` so the refinement loop can round-trip a weight
|
| 508 |
+
(quantize then dequantize) to see what the quantizer is actually getting wrong.
|
| 509 |
+
"""
|
| 510 |
+
layout = get_layout_class(QUANT_ALGOS[fmt]["comfy_tensor_layout"])
|
| 511 |
+
if fmt == "int8_tensorwise":
|
| 512 |
+
qdata, params = layout.quantize(
|
| 513 |
+
weight, is_weight=True, per_channel=True,
|
| 514 |
+
convrot=True, convrot_groupsize=groupsize,
|
| 515 |
+
)
|
| 516 |
+
conf = {"format": "int8_tensorwise", "convrot": True, "convrot_groupsize": groupsize}
|
| 517 |
+
elif fmt == "convrot_w4a4":
|
| 518 |
+
qdata, params = layout.quantize(weight, convrot_groupsize=groupsize)
|
| 519 |
+
conf = {"format": "convrot_w4a4", "convrot_groupsize": groupsize,
|
| 520 |
+
"linear_dtype": getattr(params, "linear_dtype", "int4")}
|
| 521 |
+
elif fmt == "float8_e4m3fn":
|
| 522 |
+
qdata, params = layout.quantize(weight, scale="recalculate")
|
| 523 |
+
conf = {"format": "float8_e4m3fn"}
|
| 524 |
+
else:
|
| 525 |
+
raise ValueError(fmt)
|
| 526 |
+
return qdata, params, layout, conf
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
def quantize_weight(weight: torch.Tensor, fmt: str, groupsize: int):
|
| 530 |
+
qdata, params, _, conf = _quantize_raw(weight, fmt, groupsize)
|
| 531 |
+
return qdata, {"weight_scale": params.scale}, conf
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
def conf_tensor(conf: dict) -> torch.Tensor:
|
| 535 |
+
return torch.tensor(list(json.dumps(conf).encode("utf-8")), dtype=torch.uint8)
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
def load_act_stats(path: str) -> dict[str, torch.Tensor]:
|
| 539 |
+
"""Read the file the capture nodes write: {layer name: per-input-channel RMS}."""
|
| 540 |
+
stats = {}
|
| 541 |
+
with safe_open(path, framework="pt", device="cpu") as handle:
|
| 542 |
+
for key in handle.keys():
|
| 543 |
+
stats[key] = handle.get_tensor(key)
|
| 544 |
+
if not stats:
|
| 545 |
+
raise RuntimeError("{} contains no activation statistics".format(path))
|
| 546 |
+
return stats
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
def check_act_stats_coverage(stats: dict, keys, prefix: str, ranks: dict) -> None:
|
| 550 |
+
"""Fail before any GPU work if the statistics don't cover every layer that will branch.
|
| 551 |
+
|
| 552 |
+
Partial statistics are the quiet-wrongness case: the covered layers would get an
|
| 553 |
+
activation-aware split and the rest a plain one, producing a checkpoint that is neither,
|
| 554 |
+
and no measurement of it would mean anything.
|
| 555 |
+
|
| 556 |
+
Deliberately key-only. Which layers branch depends on `is_target` plus a positive
|
| 557 |
+
`rank_for_leaf`, both of which read the layer *name*; the one weight-dependent step in
|
| 558 |
+
the loop (`min(wanted, min(w.shape))`) can only lower a rank, never raise a zero above
|
| 559 |
+
it, so this pre-pass sees exactly the set the loop will branch. Doing it here instead of
|
| 560 |
+
after the loop turns a ~6 minute mistake into a two-second one.
|
| 561 |
+
"""
|
| 562 |
+
covered, missing = 0, []
|
| 563 |
+
for key in keys:
|
| 564 |
+
if not key.endswith(".weight"):
|
| 565 |
+
continue
|
| 566 |
+
layer = key[: -len(".weight")]
|
| 567 |
+
if not is_target(layer, prefix):
|
| 568 |
+
continue
|
| 569 |
+
if rank_for_leaf(ranks, leaf_name(layer, prefix)) <= 0:
|
| 570 |
+
continue
|
| 571 |
+
if stats.get(layer[len(prefix):] if prefix else layer) is None:
|
| 572 |
+
missing.append(layer)
|
| 573 |
+
else:
|
| 574 |
+
covered += 1
|
| 575 |
+
if missing:
|
| 576 |
+
raise RuntimeError(
|
| 577 |
+
"activation stats cover {} of {} branched layers; {} are missing, e.g. {}.\n"
|
| 578 |
+
"The capture run did not see every layer -- usually because sampling was "
|
| 579 |
+
"interrupted, or the statistics came from a different model. Re-capture rather "
|
| 580 |
+
"than building a checkpoint that is activation-aware in some layers and not "
|
| 581 |
+
"others.".format(covered, covered + len(missing), len(missing),
|
| 582 |
+
", ".join(missing[:3])))
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
def convert(src: str, dst: str, fmt: str, groupsize: int, device: str = "cuda", rank: int = 0,
|
| 586 |
+
refine_iters: int = 100, variant: str = "unknown", progress_cb=None,
|
| 587 |
+
rank_alloc: str = "uniform", act_stats: str | None = None,
|
| 588 |
+
weight_patch=None, refine_tol: float = REFINE_TOL,
|
| 589 |
+
seed: int | None = DEFAULT_SEED):
|
| 590 |
+
"""Quantize `src` into `dst`. Returns the summary line it printed.
|
| 591 |
+
|
| 592 |
+
`rank` is a budget, `rank_alloc` decides how it is spread across the eight projection
|
| 593 |
+
types -- see `RANK_ALLOCATIONS`. Non-uniform allocations keep the same total branch bytes.
|
| 594 |
+
|
| 595 |
+
`weight_patch(key, tensor) -> tensor` runs on every tensor as it is read, before anything
|
| 596 |
+
is quantized. `tools/bake_adapter.py` uses it to add a LoRA delta into the high-precision
|
| 597 |
+
weight, so the low-rank branch is fitted against the *merged* weight rather than the LoRA
|
| 598 |
+
being requantized on top of a finished checkpoint. Streaming it here rather than writing a
|
| 599 |
+
merged source out first matters in practice: the bf16 source is 24 GB.
|
| 600 |
+
|
| 601 |
+
`progress_cb(done, total, message)` is called as layers complete, for callers with a
|
| 602 |
+
progress bar to drive (the ComfyUI node). Errors are `RuntimeError`, never `SystemExit`:
|
| 603 |
+
`SystemExit` derives from `BaseException`, so raising it from inside a node would slip
|
| 604 |
+
past ComfyUI's executor instead of being reported as a failed node. `main()` translates
|
| 605 |
+
them back for the CLI.
|
| 606 |
+
"""
|
| 607 |
+
out: dict[str, torch.Tensor] = {}
|
| 608 |
+
quantized = 0
|
| 609 |
+
kept = 0
|
| 610 |
+
branched = 0
|
| 611 |
+
observed_leaves: set[str] = set()
|
| 612 |
+
ranks = leaf_ranks(rank, rank_alloc) if rank > 0 else {}
|
| 613 |
+
used_ranks: set[int] = set()
|
| 614 |
+
clamped: dict[str, int] = {}
|
| 615 |
+
stats = load_act_stats(act_stats) if act_stats else {}
|
| 616 |
+
t0 = time.time()
|
| 617 |
+
|
| 618 |
+
with safe_open(src, framework="pt", device="cpu") as handle:
|
| 619 |
+
keys = list(handle.keys())
|
| 620 |
+
prefix = detect_prefix(keys)
|
| 621 |
+
check_requantizable(handle, keys, prefix)
|
| 622 |
+
if stats:
|
| 623 |
+
check_act_stats_coverage(stats, keys, prefix, ranks)
|
| 624 |
+
|
| 625 |
+
# Companion keys (old scales/markers) for target layers get regenerated fresh
|
| 626 |
+
# when we process the ".weight" key below -- skip the stale copies on disk,
|
| 627 |
+
# otherwise they'd overwrite our new ones later in iteration order.
|
| 628 |
+
stale_companions = set()
|
| 629 |
+
for key in keys:
|
| 630 |
+
if key.endswith(".weight"):
|
| 631 |
+
layer = key[: -len(".weight")]
|
| 632 |
+
if is_target(layer, prefix):
|
| 633 |
+
for suffix in ("weight_scale", "weight_scale_2", "input_scale", "comfy_quant"):
|
| 634 |
+
stale_companions.add("{}.{}".format(layer, suffix))
|
| 635 |
+
|
| 636 |
+
for i, key in enumerate(keys):
|
| 637 |
+
if key.endswith(".weight"):
|
| 638 |
+
layer = key[: -len(".weight")]
|
| 639 |
+
leaf = leaf_name(layer, prefix)
|
| 640 |
+
if leaf is not None:
|
| 641 |
+
observed_leaves.add(leaf)
|
| 642 |
+
if is_target(layer, prefix):
|
| 643 |
+
w = dequantize_target_weight(handle, layer, device)
|
| 644 |
+
if weight_patch is not None:
|
| 645 |
+
w = weight_patch(key, w)
|
| 646 |
+
# A reallocated budget can ask for more rank than the weight has singular
|
| 647 |
+
# directions (attn.wk is only 1536 wide). `svd_lowrank` clamps internally,
|
| 648 |
+
# but clamping here too keeps the reported rank honest.
|
| 649 |
+
wanted = rank_for_leaf(ranks, leaf)
|
| 650 |
+
leaf_rank = min(wanted, min(w.shape))
|
| 651 |
+
if leaf and leaf_rank < wanted:
|
| 652 |
+
clamped[leaf] = leaf_rank
|
| 653 |
+
if leaf_rank > 0:
|
| 654 |
+
# Stats are keyed by module path (`blocks.0.attn.wq`); checkpoint
|
| 655 |
+
# keys may carry a prefix, so strip it to look them up.
|
| 656 |
+
# Coverage was already proven complete before the loop started, so
|
| 657 |
+
# a None here cannot happen for a branched layer.
|
| 658 |
+
act = stats.get(layer[len(prefix):] if prefix else layer)
|
| 659 |
+
split = svdquant_split(w, leaf_rank, fmt, groupsize, refine_iters,
|
| 660 |
+
act_rms=act, refine_tol=refine_tol,
|
| 661 |
+
seed=_layer_seed(seed, layer))
|
| 662 |
+
if split is None:
|
| 663 |
+
print(" warning: {} is degenerate (zero or non-finite); "
|
| 664 |
+
"quantizing it without a low-rank branch".format(layer),
|
| 665 |
+
flush=True)
|
| 666 |
+
else:
|
| 667 |
+
w, l1, l2 = split
|
| 668 |
+
out["{}.svdq_l1".format(layer)] = l1.cpu()
|
| 669 |
+
out["{}.svdq_l2".format(layer)] = l2.cpu()
|
| 670 |
+
branched += 1
|
| 671 |
+
used_ranks.add(int(l1.shape[1]))
|
| 672 |
+
del l1, l2
|
| 673 |
+
qdata, scales, conf = quantize_weight(w, fmt, groupsize)
|
| 674 |
+
out[key] = qdata.cpu()
|
| 675 |
+
for name, value in scales.items():
|
| 676 |
+
out["{}.{}".format(layer, name)] = value.cpu()
|
| 677 |
+
out["{}.comfy_quant".format(layer)] = conf_tensor(conf)
|
| 678 |
+
del w, qdata, scales
|
| 679 |
+
quantized += 1
|
| 680 |
+
if progress_cb is not None:
|
| 681 |
+
progress_cb(quantized, _EXPECTED_LAYERS,
|
| 682 |
+
"quantized {} layers ({:.0f}s)".format(
|
| 683 |
+
quantized, time.time() - t0))
|
| 684 |
+
if quantized % 32 == 0:
|
| 685 |
+
torch.cuda.empty_cache()
|
| 686 |
+
print(f" [{i + 1}/{len(keys)}] quantized {quantized} layers "
|
| 687 |
+
f"({time.time() - t0:.0f}s)", flush=True)
|
| 688 |
+
continue
|
| 689 |
+
if key in stale_companions:
|
| 690 |
+
continue
|
| 691 |
+
value = handle.get_tensor(key)
|
| 692 |
+
# The non-quantized layers (txtfusion, embeddings) can carry a LoRA too, and they
|
| 693 |
+
# are the half a bake would otherwise silently drop.
|
| 694 |
+
out[key] = value if weight_patch is None else weight_patch(key, value)
|
| 695 |
+
kept += 1
|
| 696 |
+
|
| 697 |
+
gc.collect()
|
| 698 |
+
torch.cuda.empty_cache()
|
| 699 |
+
|
| 700 |
+
if quantized == 0:
|
| 701 |
+
raise RuntimeError(
|
| 702 |
+
"Found the transformer blocks but quantized nothing: no layer under "
|
| 703 |
+
"'{}blocks.' ends in one of the expected leaf names.\n"
|
| 704 |
+
" expected: {}\n"
|
| 705 |
+
" observed: {}\n"
|
| 706 |
+
"This is a Krea 2 variant with different layer naming; quantize_krea2.py "
|
| 707 |
+
"would otherwise have written a full-size file that does nothing."
|
| 708 |
+
.format(prefix, ", ".join(sorted(_QUANT_SUFFIXES)),
|
| 709 |
+
", ".join(sorted(observed_leaves)) or "none")
|
| 710 |
+
)
|
| 711 |
+
|
| 712 |
+
# A leaf can only be clamped when the allocation asked for more rank than the weight is
|
| 713 |
+
# wide, which means part of the budget went unspent and the file is *smaller* than the
|
| 714 |
+
# uniform one -- so a comparison against uniform at this budget is no longer like-for-like.
|
| 715 |
+
if clamped:
|
| 716 |
+
print("warning: rank allocation {!r} asked for more rank than these layers are wide, "
|
| 717 |
+
"so they were capped and part of the budget went unspent: {}. The checkpoint is "
|
| 718 |
+
"smaller than uniform rank {}, not the same size -- lower the budget to compare "
|
| 719 |
+
"them fairly.".format(
|
| 720 |
+
rank_alloc, ", ".join("{}={}".format(k, v) for k, v in sorted(clamped.items())),
|
| 721 |
+
rank), flush=True)
|
| 722 |
+
|
| 723 |
+
created = len(out) - kept - quantized
|
| 724 |
+
factors = "" if not rank else " + {} low-rank factors".format(branched * 2)
|
| 725 |
+
print(f"quantized {quantized} layers; {kept} tensors passed through; "
|
| 726 |
+
f"{created} tensors created (qdata/scale/marker{factors}); "
|
| 727 |
+
f"{len(out)} tensors total; writing {dst} ...", flush=True)
|
| 728 |
+
|
| 729 |
+
# Recorded so the loader (and anyone inspecting the file) can tell what produced it.
|
| 730 |
+
# safetensors metadata is str -> str only. The loader treats all of this as optional:
|
| 731 |
+
# checkpoints published before this existed still load, with the rank recovered from
|
| 732 |
+
# the svdq_l1 shape as before.
|
| 733 |
+
# The loader trusts the factor shapes over `krea2_svdquant_rank`, and under a non-uniform
|
| 734 |
+
# allocation no single number is the rank -- so record the budget, the allocation name and
|
| 735 |
+
# the full per-leaf map, and leave `rank` meaning "budget" for older loaders reading it.
|
| 736 |
+
metadata = {
|
| 737 |
+
"krea2_svdquant_tool_version": __version__,
|
| 738 |
+
"krea2_svdquant_format": fmt,
|
| 739 |
+
"krea2_svdquant_rank": str(rank),
|
| 740 |
+
"krea2_svdquant_rank_alloc": rank_alloc,
|
| 741 |
+
"krea2_svdquant_rank_map": json.dumps(ranks, sort_keys=True) if ranks else "",
|
| 742 |
+
"krea2_svdquant_groupsize": str(groupsize),
|
| 743 |
+
"krea2_svdquant_refine_iters": str(refine_iters if rank else 0),
|
| 744 |
+
"krea2_svdquant_variant": variant,
|
| 745 |
+
"krea2_svdquant_quantized_layers": str(quantized),
|
| 746 |
+
"krea2_svdquant_branched_layers": str(branched),
|
| 747 |
+
"krea2_svdquant_source_name": os.path.basename(src),
|
| 748 |
+
"krea2_svdquant_source_bytes": str(os.path.getsize(src)),
|
| 749 |
+
# Two checkpoints with identical rank/refine settings are still different models if
|
| 750 |
+
# one was activation-weighted, so this has to be recoverable from the file.
|
| 751 |
+
"krea2_svdquant_act_stats": os.path.basename(act_stats) if act_stats else "",
|
| 752 |
+
# A build is reproducible only against the same seed AND the same device: the low-rank
|
| 753 |
+
# split draws a random probe matrix, and CPU and CUDA neither generate the same numbers
|
| 754 |
+
# from a seed nor reduce their GEMMs in the same order. Both go in the file so "why is
|
| 755 |
+
# my checkpoint not byte-identical to yours" is answerable from the file.
|
| 756 |
+
"krea2_svdquant_seed": "" if seed is None else str(seed),
|
| 757 |
+
"krea2_svdquant_device": str(device),
|
| 758 |
+
}
|
| 759 |
+
if progress_cb is not None:
|
| 760 |
+
progress_cb(quantized, _EXPECTED_LAYERS, "writing {:.2f} GB ...".format(
|
| 761 |
+
sum(t.numel() * t.element_size() for t in out.values()) / 1024 ** 3))
|
| 762 |
+
save_file(out, dst, metadata=metadata)
|
| 763 |
+
size = os.path.getsize(dst) / 1024 ** 3
|
| 764 |
+
rank_desc = ""
|
| 765 |
+
if used_ranks:
|
| 766 |
+
rank_desc = (" rank {}".format(next(iter(used_ranks))) if len(used_ranks) == 1
|
| 767 |
+
else " rank {}-{} ({})".format(min(used_ranks), max(used_ranks),
|
| 768 |
+
rank_alloc))
|
| 769 |
+
summary = ("quantized {} layers ({} branched{}) in {:.0f}s -> {} ({:.2f} GB, source "
|
| 770 |
+
"{:.2f} GB)".format(quantized, branched, rank_desc, time.time() - t0, dst, size,
|
| 771 |
+
os.path.getsize(src) / 1024 ** 3))
|
| 772 |
+
print("done in {:.0f}s -> {} ({:.2f} GB, source {:.2f} GB)".format(
|
| 773 |
+
time.time() - t0, dst, size, os.path.getsize(src) / 1024 ** 3))
|
| 774 |
+
return summary
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
# The Krea 2 block count, used only to scale a progress bar. A variant with a different
|
| 778 |
+
# depth still quantizes correctly; the bar is just less accurate.
|
| 779 |
+
_EXPECTED_LAYERS = 28 * len(_QUANT_SUFFIXES)
|
| 780 |
+
|
| 781 |
+
_FORMAT_ALIASES = {
|
| 782 |
+
"int8": "int8_tensorwise",
|
| 783 |
+
"w4a4": "convrot_w4a4",
|
| 784 |
+
"svdq": "convrot_w4a4",
|
| 785 |
+
"fp8": "float8_e4m3fn",
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
_GENERIC_STEMS = ("raw", "model", "diffusion_pytorch_model", "turbo")
|
| 789 |
+
|
| 790 |
+
SAMPLER_HINTS = {
|
| 791 |
+
"base": ("Base (non-distilled) model: start from ~50 steps, cfg 3.5, euler/simple, and a "
|
| 792 |
+
"real negative prompt. See workflows/krea2_base_svdquant_w4a4_t2i.json."),
|
| 793 |
+
"turbo": ("Turbo (distilled) model: 8 steps, cfg 1.0, euler/simple. "
|
| 794 |
+
"See workflows/krea2_turbo_svdquant_w4a4_t2i.json."),
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
def resolve_format(fmt_name: str, rank: int, rank_was_set: bool = True) -> tuple[str, int]:
|
| 799 |
+
"""Map a CLI/node format name to a comfy_kitchen format, validating the rank with it.
|
| 800 |
+
|
| 801 |
+
Shared by `main()` and the ComfyUI node so the two cannot drift apart on which
|
| 802 |
+
combinations are legal.
|
| 803 |
+
"""
|
| 804 |
+
fmt = _FORMAT_ALIASES[fmt_name]
|
| 805 |
+
if fmt not in QUANT_ALGOS:
|
| 806 |
+
raise RuntimeError("{} is not available in this ComfyUI build".format(fmt))
|
| 807 |
+
# Silently zeroing the rank here used to make `--format w4a4 --rank 128` look like it had
|
| 808 |
+
# done something it had not.
|
| 809 |
+
if fmt_name != "svdq" and rank_was_set:
|
| 810 |
+
raise RuntimeError(
|
| 811 |
+
"rank only applies to format 'svdq' (you asked for '{}'). The low-rank branch is "
|
| 812 |
+
"what distinguishes svdq from plain w4a4.".format(fmt_name))
|
| 813 |
+
if fmt_name == "svdq" and rank <= 0:
|
| 814 |
+
raise RuntimeError("format 'svdq' needs rank > 0; use format 'w4a4' for no branch")
|
| 815 |
+
return fmt, (rank if fmt_name == "svdq" else 0)
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
def derive_out_path(src: str, fmt_name: str, rank: int, variant: str,
|
| 819 |
+
rank_alloc: str = "uniform", act_stats: str | None = None
|
| 820 |
+
) -> tuple[str, str | None]:
|
| 821 |
+
"""The default output path, plus a note to show when the source name is uninformative."""
|
| 822 |
+
stem = os.path.splitext(os.path.basename(src))[0]
|
| 823 |
+
note = None
|
| 824 |
+
if variant != "unknown":
|
| 825 |
+
stem = "Krea2-{}".format(variant.capitalize())
|
| 826 |
+
elif stem.lower() in _GENERIC_STEMS:
|
| 827 |
+
note = ("note: '{}' is a generic filename. Set the variant (or an explicit output "
|
| 828 |
+
"name) to get a checkpoint name you will still recognise next month."
|
| 829 |
+
.format(stem))
|
| 830 |
+
# The allocation goes in the filename because two checkpoints with the same budget and
|
| 831 |
+
# different allocations are the same size -- nothing else would tell them apart.
|
| 832 |
+
alloc_tag = "" if rank_alloc == "uniform" else "-{}".format(rank_alloc)
|
| 833 |
+
# Same reasoning as alloc_tag: an activation-weighted build is byte-identical in shape to
|
| 834 |
+
# a plain one, so without a tag the two are indistinguishable on disk.
|
| 835 |
+
act_tag = "-actaware" if act_stats else ""
|
| 836 |
+
suffix = ("SVDQuant-W4A4-rank{}{}{}".format(rank, alloc_tag, act_tag) if rank else
|
| 837 |
+
("{}-convrot".format(fmt_name.upper()) if fmt_name != "fp8" else "FP8"))
|
| 838 |
+
return os.path.join(os.path.dirname(src), "{}-{}.safetensors".format(stem, suffix)), note
|
| 839 |
+
|
| 840 |
+
|
| 841 |
+
def main():
|
| 842 |
+
ap = argparse.ArgumentParser()
|
| 843 |
+
ap.add_argument("src")
|
| 844 |
+
ap.add_argument("--format", choices=["int8", "w4a4", "svdq", "fp8"], default="int8",
|
| 845 |
+
help="svdq = w4a4 residual + SVDQuant low-rank bf16 branch; "
|
| 846 |
+
"fp8 = float8_e4m3fn, no convrot, no low-rank branch")
|
| 847 |
+
ap.add_argument("--groupsize", type=int, default=256, help="unused for fp8")
|
| 848 |
+
ap.add_argument("--rank", type=int, default=64, help="low-rank branch budget, svdq only")
|
| 849 |
+
ap.add_argument("--rank-alloc", choices=sorted(RANK_ALLOCATIONS), default="uniform",
|
| 850 |
+
help="how to spread the rank budget across the eight projection types. "
|
| 851 |
+
"uniform = same rank everywhere. gqa = byte-neutral reallocation "
|
| 852 |
+
"towards the GQA kv projections, which absorb ~2x the error at a "
|
| 853 |
+
"third of the branch cost (see RANK_ALLOCATIONS)")
|
| 854 |
+
ap.add_argument("--refine-iters", type=int, default=100,
|
| 855 |
+
help="svdq only: refine the low-rank branch against the quantization "
|
| 856 |
+
"error, keeping the best (0 = plain single-shot SVD, much faster "
|
| 857 |
+
"but ~10%% more reconstruction error). This is a cap; --refine-tol "
|
| 858 |
+
"is what usually stops the loop first")
|
| 859 |
+
ap.add_argument("--refine-tol", type=float, default=REFINE_TOL, metavar="FRACTION",
|
| 860 |
+
help="svdq only: stop refining a layer once an iteration improves its "
|
| 861 |
+
"reconstruction error by less than this fraction (default %(default)s "
|
| 862 |
+
"= 0.1%%). Lower means more iterations for less return: 0.001 takes "
|
| 863 |
+
"~22 iterations per layer, 0.005 takes ~9 for 2%% more error, and 0 "
|
| 864 |
+
"restores the old behaviour of running nearly all --refine-iters")
|
| 865 |
+
ap.add_argument("--variant", choices=["turbo", "base", "unknown"], default="unknown",
|
| 866 |
+
help="which Krea 2 release this is. Only affects the output filename "
|
| 867 |
+
"and the recorded metadata -- quantization is identical for both, "
|
| 868 |
+
"the difference is the sampler settings you run afterwards")
|
| 869 |
+
ap.add_argument("--act-stats", default=None, metavar="PATH",
|
| 870 |
+
help="svdq only: activation statistics from the Krea2 SVDQuant Capture "
|
| 871 |
+
"nodes. Weights the low-rank split by per-input-channel activation "
|
| 872 |
+
"RMS, so the branch spends its capacity on the directions the model "
|
| 873 |
+
"actually drives instead of on the largest weights. Costs nothing "
|
| 874 |
+
"at runtime -- same rank, format, size and kernel")
|
| 875 |
+
ap.add_argument("--out", default=None)
|
| 876 |
+
ap.add_argument("--device", default="cuda")
|
| 877 |
+
ap.add_argument("--seed", type=int, default=DEFAULT_SEED,
|
| 878 |
+
help="seed for the randomized low-rank SVD, so a build is reproducible "
|
| 879 |
+
"(default %(default)s). Reproducible on the same device only -- a "
|
| 880 |
+
"CPU build and a CUDA build of the same checkpoint differ by ~1e-4 "
|
| 881 |
+
"per weight whatever the seed. Pass -1 for the old unseeded "
|
| 882 |
+
"behaviour, where even two runs on one GPU differ")
|
| 883 |
+
args = ap.parse_args()
|
| 884 |
+
|
| 885 |
+
if args.act_stats:
|
| 886 |
+
if args.format != "svdq":
|
| 887 |
+
raise SystemExit("--act-stats only applies to format 'svdq': it weights the "
|
| 888 |
+
"low-rank split, and the other formats have no branch")
|
| 889 |
+
if not os.path.exists(args.act_stats):
|
| 890 |
+
raise SystemExit("--act-stats file not found: {}".format(args.act_stats))
|
| 891 |
+
|
| 892 |
+
# RuntimeError is the shared failure type (see `convert`); the CLI wants SystemExit so it
|
| 893 |
+
# prints one clean line instead of a traceback.
|
| 894 |
+
try:
|
| 895 |
+
fmt, rank = resolve_format(args.format, args.rank,
|
| 896 |
+
rank_was_set=args.rank != ap.get_default("rank"))
|
| 897 |
+
except RuntimeError as exc:
|
| 898 |
+
raise SystemExit(str(exc)) from None
|
| 899 |
+
|
| 900 |
+
if args.rank_alloc != "uniform" and not rank:
|
| 901 |
+
raise SystemExit("--rank-alloc only applies to format 'svdq'")
|
| 902 |
+
|
| 903 |
+
out = args.out
|
| 904 |
+
if out is None:
|
| 905 |
+
out, note = derive_out_path(args.src, args.format, rank, args.variant, args.rank_alloc,
|
| 906 |
+
args.act_stats)
|
| 907 |
+
if note:
|
| 908 |
+
print(note, flush=True)
|
| 909 |
+
try:
|
| 910 |
+
convert(args.src, out, fmt, args.groupsize, args.device, rank, args.refine_iters,
|
| 911 |
+
variant=args.variant, rank_alloc=args.rank_alloc, act_stats=args.act_stats,
|
| 912 |
+
refine_tol=args.refine_tol,
|
| 913 |
+
seed=None if args.seed < 0 else args.seed)
|
| 914 |
+
except RuntimeError as exc:
|
| 915 |
+
raise SystemExit(str(exc)) from None
|
| 916 |
+
|
| 917 |
+
hint = SAMPLER_HINTS.get(args.variant)
|
| 918 |
+
if hint:
|
| 919 |
+
print("\n" + hint)
|
| 920 |
+
|
| 921 |
+
|
| 922 |
+
if __name__ == "__main__":
|
| 923 |
+
main()
|
svdquant_capture.py
CHANGED
|
@@ -228,9 +228,14 @@ class Krea2SVDQuantCaptureSave:
|
|
| 228 |
}
|
| 229 |
}
|
| 230 |
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
OUTPUT_NODE = True
|
| 235 |
FUNCTION = "save"
|
| 236 |
CATEGORY = _CATEGORY
|
|
@@ -243,7 +248,24 @@ class Krea2SVDQuantCaptureSave:
|
|
| 243 |
return float("nan")
|
| 244 |
|
| 245 |
def save(self, latent, filename, keep_capturing):
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
info = write(path)
|
| 248 |
if not keep_capturing:
|
| 249 |
detach()
|
|
@@ -251,7 +273,7 @@ class Krea2SVDQuantCaptureSave:
|
|
| 251 |
"tokens per layer: {min_tokens}-{max_tokens}{more}").format(
|
| 252 |
more="" if keep_capturing else "\nhooks detached", **info)
|
| 253 |
logging.info("[krea2-svdquant] %s", status.replace("\n", " "))
|
| 254 |
-
return {"ui": {"text": [status]}, "result": (latent, status)}
|
| 255 |
|
| 256 |
|
| 257 |
NODE_CLASS_MAPPINGS = {
|
|
|
|
| 228 |
}
|
| 229 |
}
|
| 230 |
|
| 231 |
+
# `act_stats_path` is appended rather than inserted so workflows saved against the
|
| 232 |
+
# two-output version keep their links: slot indices 0 and 1 still mean what they did.
|
| 233 |
+
RETURN_TYPES = ("LATENT", "STRING", "STRING")
|
| 234 |
+
RETURN_NAMES = ("latent", "status", "act_stats_path")
|
| 235 |
+
OUTPUT_TOOLTIPS = ("Passed through unchanged.", "Where the file went and what is in it.",
|
| 236 |
+
"Absolute path of the file just written. Wire it into the Quantize "
|
| 237 |
+
"node's act_stats: that is also what forces the quantizer to run "
|
| 238 |
+
"after the calibration pass rather than before it.")
|
| 239 |
OUTPUT_NODE = True
|
| 240 |
FUNCTION = "save"
|
| 241 |
CATEGORY = _CATEGORY
|
|
|
|
| 248 |
return float("nan")
|
| 249 |
|
| 250 |
def save(self, latent, filename, keep_capturing):
|
| 251 |
+
# Relative to ComfyUI/output/, which is what the tooltip promises. `os.path.join`
|
| 252 |
+
# alone does not keep that promise: hand it an absolute path and it discards the
|
| 253 |
+
# first argument, and `write()` then happily creates whatever directory it names.
|
| 254 |
+
# The sibling Quantize node already resolves `act_stats` this way, so make the two
|
| 255 |
+
# agree about what a bare filename means.
|
| 256 |
+
filename = filename.strip()
|
| 257 |
+
if not filename:
|
| 258 |
+
raise ValueError("filename is empty")
|
| 259 |
+
if os.path.isabs(filename) or os.path.splitdrive(filename)[0]:
|
| 260 |
+
raise ValueError(
|
| 261 |
+
"filename must be relative to ComfyUI/output/, got an absolute path: {}"
|
| 262 |
+
.format(filename))
|
| 263 |
+
out_dir = folder_paths.get_output_directory()
|
| 264 |
+
path = os.path.normpath(os.path.join(out_dir, filename))
|
| 265 |
+
if os.path.commonpath([os.path.abspath(out_dir), os.path.abspath(path)]) != \
|
| 266 |
+
os.path.abspath(out_dir):
|
| 267 |
+
raise ValueError(
|
| 268 |
+
"filename must stay under ComfyUI/output/, got: {}".format(filename))
|
| 269 |
info = write(path)
|
| 270 |
if not keep_capturing:
|
| 271 |
detach()
|
|
|
|
| 273 |
"tokens per layer: {min_tokens}-{max_tokens}{more}").format(
|
| 274 |
more="" if keep_capturing else "\nhooks detached", **info)
|
| 275 |
logging.info("[krea2-svdquant] %s", status.replace("\n", " "))
|
| 276 |
+
return {"ui": {"text": [status]}, "result": (latent, status, info["path"])}
|
| 277 |
|
| 278 |
|
| 279 |
NODE_CLASS_MAPPINGS = {
|
svdquant_lora.py
CHANGED
|
@@ -54,12 +54,12 @@ from .svdquant_w4a4 import add_low_rank, has_branch
|
|
| 54 |
_PREFIX = "diffusion_model."
|
| 55 |
_ALT_PREFIX = "transformer."
|
| 56 |
|
| 57 |
-
# What to do with an adapter that cannot fold into the low-rank branch (LoKr, LoHa, OFT).
|
| 58 |
-
# `bypass` computes it every forward and never touches the 4-bit weight; `
|
| 59 |
-
#
|
| 60 |
-
#
|
| 61 |
-
# nothing and quantizes the delta to 4 bits along with the weight.
|
| 62 |
ADAPTER_BYPASS = "bypass (exact, slower)"
|
|
|
|
| 63 |
ADAPTER_BAKE = "bake into the weight (fast, requantizes the delta)"
|
| 64 |
|
| 65 |
# Reloading a 300 MB LoRA off disk on every graph execution is pure latency when the
|
|
@@ -135,6 +135,59 @@ def _plain_lora_factors(adapter, strength: float, device, dtype):
|
|
| 135 |
return l1.contiguous(), l2.contiguous()
|
| 136 |
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
def _supports_bypass(adapter) -> bool:
|
| 139 |
"""True when the adapter can run without touching the base weight.
|
| 140 |
|
|
@@ -244,6 +297,17 @@ def collect_svdquant_lora(patcher, lora_sd, strength: float, quant_layers: set[s
|
|
| 244 |
# A plain LoRA folds into the branch for free in either mode -- one pair of
|
| 245 |
# GEMMs for the whole stack -- so `bake` has nothing to win here.
|
| 246 |
into_branch.setdefault(layer, []).append(factors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
elif (adapter_mode == ADAPTER_BYPASS
|
| 248 |
and isinstance(patch, comfy.weight_adapter.WeightAdapterBase)
|
| 249 |
and _supports_bypass(patch)):
|
|
@@ -293,15 +357,14 @@ class Krea2SVDQuantLoraLoader:
|
|
| 293 |
}),
|
| 294 |
},
|
| 295 |
"optional": {
|
| 296 |
-
"adapters": ([ADAPTER_BYPASS, ADAPTER_BAKE], {
|
| 297 |
"default": ADAPTER_BYPASS,
|
| 298 |
-
"tooltip": "Only affects LoRAs that cannot fold into the low-rank
|
| 299 |
-
"(LoKr, LoHa, OFT); a plain LoRA is free
|
| 300 |
-
"computes the adapter every forward
|
| 301 |
-
"4-bit weight. '
|
| 302 |
-
"
|
| 303 |
-
"
|
| 304 |
-
"+1.8 s per model call, bake +0.",
|
| 305 |
}),
|
| 306 |
},
|
| 307 |
}
|
|
|
|
| 54 |
_PREFIX = "diffusion_model."
|
| 55 |
_ALT_PREFIX = "transformer."
|
| 56 |
|
| 57 |
+
# What to do with an adapter that cannot fold directly into the low-rank branch (LoKr, LoHa, OFT).
|
| 58 |
+
# `bypass` computes it every forward and never touches the 4-bit weight; `svd delta` decomposes
|
| 59 |
+
# the weight delta with torch.svd_lowrank and merges it into the low-rank branch (zero per-step
|
| 60 |
+
# overhead); `bake` hands it to ComfyUI, which rewrites the weight once and pays nothing per step.
|
|
|
|
| 61 |
ADAPTER_BYPASS = "bypass (exact, slower)"
|
| 62 |
+
ADAPTER_SVD = "svd delta (fast, low-rank SVD, no per-step overhead)"
|
| 63 |
ADAPTER_BAKE = "bake into the weight (fast, requantizes the delta)"
|
| 64 |
|
| 65 |
# Reloading a 300 MB LoRA off disk on every graph execution is pure latency when the
|
|
|
|
| 135 |
return l1.contiguous(), l2.contiguous()
|
| 136 |
|
| 137 |
|
| 138 |
+
def _svd_adapter_factors(adapter, strength: float, device, dtype, max_rank: int = 64):
|
| 139 |
+
"""Decompose an arbitrary adapter's weight delta into low-rank factors (l1, l2) via SVD.
|
| 140 |
+
|
| 141 |
+
For LoKr, LoHa, OFT, GFS, etc. This extracts the weight delta, computes a
|
| 142 |
+
truncated SVD using torch.svd_lowrank, and produces (l1, l2) where l1 @ l2 ≈ delta * strength.
|
| 143 |
+
Running this in the low-rank branch completely eliminates the per-step runtime overhead
|
| 144 |
+
of complex adapter contractions while preserving 4-bit base weights untouched.
|
| 145 |
+
"""
|
| 146 |
+
try:
|
| 147 |
+
delta = None
|
| 148 |
+
# Case 1: ComfyUI WeightAdapterBase with calculate_weight
|
| 149 |
+
if hasattr(adapter, "calculate_weight"):
|
| 150 |
+
shape = getattr(adapter, "shape", None)
|
| 151 |
+
if shape is not None and len(shape) == 2:
|
| 152 |
+
zeros = torch.zeros(shape, dtype=torch.float32, device="cpu")
|
| 153 |
+
delta = adapter.calculate_weight(zeros)
|
| 154 |
+
# Case 2: Tuple / Diff / weights directly
|
| 155 |
+
if delta is None and hasattr(adapter, "weights"):
|
| 156 |
+
weights = adapter.weights
|
| 157 |
+
if weights and isinstance(adapter, comfy.weight_adapter.DiffAdapter):
|
| 158 |
+
delta = weights[0]
|
| 159 |
+
elif isinstance(adapter, comfy.weight_adapter.LoKrAdapter) and len(weights) >= 2:
|
| 160 |
+
w1, w2 = weights[0], weights[1]
|
| 161 |
+
if w1 is not None and w2 is not None:
|
| 162 |
+
delta = torch.kron(w1.to(torch.float32), w2.to(torch.float32))
|
| 163 |
+
elif isinstance(adapter, comfy.weight_adapter.LoHaAdapter) and len(weights) >= 4:
|
| 164 |
+
w1a, w1b, w2a, w2b = weights[:4]
|
| 165 |
+
if all(w is not None for w in (w1a, w1b, w2a, w2b)):
|
| 166 |
+
delta = (w1a.to(torch.float32) @ w1b.to(torch.float32)) * (w2a.to(torch.float32) @ w2b.to(torch.float32))
|
| 167 |
+
elif isinstance(adapter, tuple) and len(adapter) >= 1 and torch.is_tensor(adapter[0]):
|
| 168 |
+
delta = adapter[0]
|
| 169 |
+
|
| 170 |
+
if delta is None or delta.ndim != 2:
|
| 171 |
+
return None
|
| 172 |
+
|
| 173 |
+
out_dim, in_dim = delta.shape
|
| 174 |
+
k = min(max_rank, min(out_dim, in_dim))
|
| 175 |
+
if k <= 0:
|
| 176 |
+
return None
|
| 177 |
+
|
| 178 |
+
delta_f32 = delta.to(device="cpu", dtype=torch.float32)
|
| 179 |
+
# SVD: delta_f32 ≈ U @ diag(S) @ V.T
|
| 180 |
+
U, S, V = torch.svd_lowrank(delta_f32, q=k)
|
| 181 |
+
sqrt_s = torch.sqrt(torch.clamp(S, min=0.0))
|
| 182 |
+
# l1 is (out_dim, k), l2 is (k, in_dim)
|
| 183 |
+
l1 = (U * sqrt_s.unsqueeze(0)).to(device=device, dtype=dtype)
|
| 184 |
+
l2 = (sqrt_s.unsqueeze(1) * V.T * strength).to(device=device, dtype=dtype)
|
| 185 |
+
return l1.contiguous(), l2.contiguous()
|
| 186 |
+
except Exception as exc:
|
| 187 |
+
logging.warning("[krea2-svdquant] SVD adapter decomposition skipped: %s", exc)
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
|
| 191 |
def _supports_bypass(adapter) -> bool:
|
| 192 |
"""True when the adapter can run without touching the base weight.
|
| 193 |
|
|
|
|
| 297 |
# A plain LoRA folds into the branch for free in either mode -- one pair of
|
| 298 |
# GEMMs for the whole stack -- so `bake` has nothing to win here.
|
| 299 |
into_branch.setdefault(layer, []).append(factors)
|
| 300 |
+
elif adapter_mode == ADAPTER_SVD:
|
| 301 |
+
# Attempt SVD decomposition into low-rank factors
|
| 302 |
+
svd_factors = _svd_adapter_factors(patch, strength, device, dtype)
|
| 303 |
+
if svd_factors is not None:
|
| 304 |
+
into_branch.setdefault(layer, []).append(svd_factors)
|
| 305 |
+
elif isinstance(patch, comfy.weight_adapter.WeightAdapterBase) and _supports_bypass(patch):
|
| 306 |
+
into_adapters.setdefault(layer, []).append((patch, strength))
|
| 307 |
+
else:
|
| 308 |
+
leftover[model_key] = patch
|
| 309 |
+
dequantizing += 1
|
| 310 |
+
continue
|
| 311 |
elif (adapter_mode == ADAPTER_BYPASS
|
| 312 |
and isinstance(patch, comfy.weight_adapter.WeightAdapterBase)
|
| 313 |
and _supports_bypass(patch)):
|
|
|
|
| 357 |
}),
|
| 358 |
},
|
| 359 |
"optional": {
|
| 360 |
+
"adapters": ([ADAPTER_BYPASS, ADAPTER_SVD, ADAPTER_BAKE], {
|
| 361 |
"default": ADAPTER_BYPASS,
|
| 362 |
+
"tooltip": "Only affects LoRAs that cannot fold directly into the low-rank "
|
| 363 |
+
"branch (LoKr, LoHa, OFT); a plain LoRA is free in all modes. "
|
| 364 |
+
"'bypass' computes the adapter every forward without touching "
|
| 365 |
+
"the 4-bit weight. 'svd delta' decomposes the delta into low-rank "
|
| 366 |
+
"factors (zero per-step overhead). 'bake' rewrites the weight once: "
|
| 367 |
+
"fast, but quantizes the delta to 4 bits.",
|
|
|
|
| 368 |
}),
|
| 369 |
},
|
| 370 |
}
|
svdquant_quantize.py
CHANGED
|
@@ -35,6 +35,13 @@ from .svdquant_diag import _CATEGORY
|
|
| 35 |
# the ~8 GB output rather than exactly it.
|
| 36 |
_MIN_FREE_BYTES = 12 * 1024 ** 3
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
def _free_bytes(path: str) -> int:
|
| 40 |
return shutil.disk_usage(os.path.dirname(os.path.abspath(path))).free
|
|
@@ -116,9 +123,64 @@ class Krea2SVDQuantQuantize:
|
|
| 116 |
"inference and the best-measured setting here - LPIPS to BF16 "
|
| 117 |
"0.3378 to 0.2825 with no LoRA. Empty means the plain objective.",
|
| 118 |
}),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
},
|
| 120 |
}
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
@classmethod
|
| 123 |
def IS_CHANGED(cls, *args, **kwargs):
|
| 124 |
# Writes an ~8 GB file as its side effect, so a cached summary would claim a file
|
|
@@ -139,7 +201,7 @@ class Krea2SVDQuantQuantize:
|
|
| 139 |
"Krea2 SVDQuant W4A4 Loader (svdq) or the stock UNETLoader (w4a4/int8/fp8).")
|
| 140 |
|
| 141 |
def run(self, source_model, format, rank, rank_alloc, refine_iters, groupsize, variant,
|
| 142 |
-
output_name, overwrite, act_stats=""):
|
| 143 |
src = folder_paths.get_full_path_or_raise("diffusion_models", source_model)
|
| 144 |
|
| 145 |
# `rank` always carries a value from the widget, so "was it set?" cannot be inferred
|
|
@@ -196,12 +258,13 @@ class Krea2SVDQuantQuantize:
|
|
| 196 |
pbar.update_absolute(done, total)
|
| 197 |
|
| 198 |
logging.info("[krea2-svdquant] quantizing %s -> %s (format %s, rank %s/%s, "
|
| 199 |
-
"refine_iters %s, act_stats %s)", src, dst, fmt, rank,
|
| 200 |
-
refine_iters if rank else 0, stats_path or "none"
|
|
|
|
| 201 |
summary = convert(src, dst, fmt, groupsize, "cuda", rank, refine_iters,
|
| 202 |
variant=variant, progress_cb=progress,
|
| 203 |
rank_alloc=rank_alloc if rank else "uniform",
|
| 204 |
-
act_stats=stats_path)
|
| 205 |
|
| 206 |
hint = SAMPLER_HINTS.get(variant)
|
| 207 |
loader = ("Krea2 SVDQuant W4A4 Loader" if rank else "the stock UNETLoader")
|
|
@@ -211,5 +274,246 @@ class Krea2SVDQuantQuantize:
|
|
| 211 |
return {"ui": {"text": [text]}, "result": (text,)}
|
| 212 |
|
| 213 |
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
# the ~8 GB output rather than exactly it.
|
| 36 |
_MIN_FREE_BYTES = 12 * 1024 ** 3
|
| 37 |
|
| 38 |
+
# All-in-one: the combined file is ~12 GB, and building one from a BF16 DiT writes an ~8 GB
|
| 39 |
+
# intermediate first. Those two can land on different drives (the intermediate goes to
|
| 40 |
+
# ComfyUI's temp directory), so they are checked separately rather than summed -- the old
|
| 41 |
+
# single 15 GB check against the output drive was under the real peak either way.
|
| 42 |
+
_MIN_FREE_ALLINONE = 15 * 1024 ** 3
|
| 43 |
+
_MIN_FREE_INTERMEDIATE = 11 * 1024 ** 3
|
| 44 |
+
|
| 45 |
|
| 46 |
def _free_bytes(path: str) -> int:
|
| 47 |
return shutil.disk_usage(os.path.dirname(os.path.abspath(path))).free
|
|
|
|
| 123 |
"inference and the best-measured setting here - LPIPS to BF16 "
|
| 124 |
"0.3378 to 0.2825 with no LoRA. Empty means the plain objective.",
|
| 125 |
}),
|
| 126 |
+
# Appended after act_stats rather than inserted among the required inputs:
|
| 127 |
+
# ComfyUI matches widgets_values positionally, so anywhere else would shift
|
| 128 |
+
# every value in a workflow saved before this input existed.
|
| 129 |
+
"seed": ("INT", {
|
| 130 |
+
"default": 0, "min": -1, "max": 0xFFFFFFFF,
|
| 131 |
+
"tooltip": "svdq only: seed for the randomized low-rank SVD. Quantizing "
|
| 132 |
+
"twice with the same seed on the same GPU now gives identical "
|
| 133 |
+
"files; -1 restores the old unseeded behaviour, where it did "
|
| 134 |
+
"not. The same seed on a different device still differs (~1e-4 "
|
| 135 |
+
"per weight) - CPU and CUDA do not draw the same numbers.",
|
| 136 |
+
}),
|
| 137 |
},
|
| 138 |
}
|
| 139 |
|
| 140 |
+
@classmethod
|
| 141 |
+
def VALIDATE_INPUTS(cls, source_model, format, variant, rank, rank_alloc, output_name,
|
| 142 |
+
overwrite):
|
| 143 |
+
"""Refuse the obviously-doomed run before the graph starts, not after.
|
| 144 |
+
|
| 145 |
+
Everything checked here is a widget, so ComfyUI has it at validation time. That
|
| 146 |
+
matters most in the calibrated graph, where this node sits behind a full sampling
|
| 147 |
+
pass: without this, "that file already exists" arrives several minutes after you
|
| 148 |
+
pressed Queue, having burned the calibration run to tell you.
|
| 149 |
+
|
| 150 |
+
Deliberately not checked here: `act_stats`, which can be wired from the Capture Save
|
| 151 |
+
node and therefore has no value yet at validation time. Its checks stay in `run`.
|
| 152 |
+
"""
|
| 153 |
+
try:
|
| 154 |
+
src = folder_paths.get_full_path_or_raise("diffusion_models", source_model)
|
| 155 |
+
except Exception as exc:
|
| 156 |
+
return str(exc)
|
| 157 |
+
try:
|
| 158 |
+
_, rank = resolve_format(format, rank, rank_was_set=False)
|
| 159 |
+
except Exception as exc:
|
| 160 |
+
return str(exc)
|
| 161 |
+
|
| 162 |
+
if output_name.strip():
|
| 163 |
+
name = output_name.strip()
|
| 164 |
+
if not name.endswith(".safetensors"):
|
| 165 |
+
name += ".safetensors"
|
| 166 |
+
dst = os.path.join(os.path.dirname(src), name)
|
| 167 |
+
else:
|
| 168 |
+
# An act-aware build lands on a different filename, and act_stats is unknowable
|
| 169 |
+
# here. Pass None and the check is against the un-tagged name: it can miss a
|
| 170 |
+
# collision, never invent one, which is the right way for a pre-flight check to
|
| 171 |
+
# be wrong.
|
| 172 |
+
dst, _note = derive_out_path(src, format, rank, variant, rank_alloc, None)
|
| 173 |
+
|
| 174 |
+
if os.path.exists(dst) and not overwrite:
|
| 175 |
+
return ("{} already exists. Enable 'overwrite', or set a different output_name."
|
| 176 |
+
.format(dst))
|
| 177 |
+
free = _free_bytes(dst)
|
| 178 |
+
if free < _MIN_FREE_BYTES:
|
| 179 |
+
return ("only {:.1f} GB free on the drive holding {}; quantizing needs roughly "
|
| 180 |
+
"{:.0f} GB of headroom.".format(free / 1024 ** 3, os.path.dirname(dst),
|
| 181 |
+
_MIN_FREE_BYTES / 1024 ** 3))
|
| 182 |
+
return True
|
| 183 |
+
|
| 184 |
@classmethod
|
| 185 |
def IS_CHANGED(cls, *args, **kwargs):
|
| 186 |
# Writes an ~8 GB file as its side effect, so a cached summary would claim a file
|
|
|
|
| 201 |
"Krea2 SVDQuant W4A4 Loader (svdq) or the stock UNETLoader (w4a4/int8/fp8).")
|
| 202 |
|
| 203 |
def run(self, source_model, format, rank, rank_alloc, refine_iters, groupsize, variant,
|
| 204 |
+
output_name, overwrite, act_stats="", seed=0):
|
| 205 |
src = folder_paths.get_full_path_or_raise("diffusion_models", source_model)
|
| 206 |
|
| 207 |
# `rank` always carries a value from the widget, so "was it set?" cannot be inferred
|
|
|
|
| 258 |
pbar.update_absolute(done, total)
|
| 259 |
|
| 260 |
logging.info("[krea2-svdquant] quantizing %s -> %s (format %s, rank %s/%s, "
|
| 261 |
+
"refine_iters %s, act_stats %s, seed %s)", src, dst, fmt, rank,
|
| 262 |
+
rank_alloc, refine_iters if rank else 0, stats_path or "none",
|
| 263 |
+
"unseeded" if seed < 0 else seed)
|
| 264 |
summary = convert(src, dst, fmt, groupsize, "cuda", rank, refine_iters,
|
| 265 |
variant=variant, progress_cb=progress,
|
| 266 |
rank_alloc=rank_alloc if rank else "uniform",
|
| 267 |
+
act_stats=stats_path, seed=None if seed < 0 else int(seed))
|
| 268 |
|
| 269 |
hint = SAMPLER_HINTS.get(variant)
|
| 270 |
loader = ("Krea2 SVDQuant W4A4 Loader" if rank else "the stock UNETLoader")
|
|
|
|
| 274 |
return {"ui": {"text": [text]}, "result": (text,)}
|
| 275 |
|
| 276 |
|
| 277 |
+
class Krea2SVDQuantQuantizeAllInOne:
|
| 278 |
+
@classmethod
|
| 279 |
+
def INPUT_TYPES(cls):
|
| 280 |
+
return {
|
| 281 |
+
"required": {
|
| 282 |
+
"source_dit": (folder_paths.get_filename_list("diffusion_models"), {
|
| 283 |
+
"tooltip": "The Krea 2 DiT model: either a BF16 source (~24 GB, will be "
|
| 284 |
+
"quantized first) or an already-quantized SVDQuant/W4A4/INT8 file.",
|
| 285 |
+
}),
|
| 286 |
+
"text_encoder": (folder_paths.get_filename_list("text_encoders"), {
|
| 287 |
+
"tooltip": "The Qwen3-VL 4B BF16 text encoder safetensors (~8.88 GB). "
|
| 288 |
+
"Its 252 linear projections will be quantized to te_format, "
|
| 289 |
+
"reducing it to ~3.2 GB without losing the vision tower.",
|
| 290 |
+
}),
|
| 291 |
+
"vae": (folder_paths.get_filename_list("vae"), {
|
| 292 |
+
"tooltip": "The Krea 2 VAE (~0.51 GB, kept unquantized).",
|
| 293 |
+
}),
|
| 294 |
+
"format": (["svdq", "w4a4", "int8", "fp8"], {
|
| 295 |
+
"default": "svdq",
|
| 296 |
+
"tooltip": "Diffusion model format. svdq: 4-bit weights and activations "
|
| 297 |
+
"plus a low-rank bf16 correction branch.",
|
| 298 |
+
}),
|
| 299 |
+
"te_format": (["w4a4", "int8"], {
|
| 300 |
+
"default": "w4a4",
|
| 301 |
+
"tooltip": "Text encoder format: w4a4 (convrot_w4a4, ~3.2 GB) or int8 "
|
| 302 |
+
"(int8_tensorwise, ~5.2 GB).",
|
| 303 |
+
}),
|
| 304 |
+
"rank": ("INT", {
|
| 305 |
+
"default": 64, "min": 8, "max": 1024, "step": 8,
|
| 306 |
+
"tooltip": "svdq only: size of the low-rank branch on the diffusion model.",
|
| 307 |
+
}),
|
| 308 |
+
"refine_iters": ("INT", {
|
| 309 |
+
"default": 100, "min": 0, "max": 200,
|
| 310 |
+
"tooltip": "svdq only (if source_dit is BF16). Refines the branch against "
|
| 311 |
+
"quantization error. Ignored if source_dit is already quantized.",
|
| 312 |
+
}),
|
| 313 |
+
"groupsize": ("INT", {
|
| 314 |
+
"default": 256, "min": 32, "max": 1024, "step": 32,
|
| 315 |
+
"tooltip": "convrot rotation group size.",
|
| 316 |
+
}),
|
| 317 |
+
"variant": (["turbo", "base", "unknown"], {
|
| 318 |
+
"default": "turbo",
|
| 319 |
+
"tooltip": "Which Krea 2 release this is.",
|
| 320 |
+
}),
|
| 321 |
+
"output_name": ("STRING", {
|
| 322 |
+
"default": "",
|
| 323 |
+
"tooltip": "Filename in models/checkpoints/. Leave empty to derive automatically.",
|
| 324 |
+
}),
|
| 325 |
+
"overwrite": ("BOOLEAN", {
|
| 326 |
+
"default": False,
|
| 327 |
+
"tooltip": "Allow overwriting existing output checkpoint.",
|
| 328 |
+
}),
|
| 329 |
+
},
|
| 330 |
+
"optional": {
|
| 331 |
+
"act_stats": ("STRING", {
|
| 332 |
+
"default": "",
|
| 333 |
+
"tooltip": "svdq only (if source_dit is BF16): activation statistics file.",
|
| 334 |
+
}),
|
| 335 |
+
"seed": ("INT", {
|
| 336 |
+
"default": 0, "min": -1, "max": 0xFFFFFFFF,
|
| 337 |
+
"tooltip": "Random seed for reproducible SVD.",
|
| 338 |
+
}),
|
| 339 |
+
},
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
@staticmethod
|
| 343 |
+
def _out_path(output_name, variant, format, rank, te_format) -> str:
|
| 344 |
+
"""Where this run will write. Shared, so validation checks the path `run` will use."""
|
| 345 |
+
ckpt_dir = folder_paths.get_folder_paths("checkpoints")[0]
|
| 346 |
+
if output_name.strip():
|
| 347 |
+
name = output_name.strip()
|
| 348 |
+
if not name.endswith(".safetensors"):
|
| 349 |
+
name += ".safetensors"
|
| 350 |
+
return os.path.join(ckpt_dir, name)
|
| 351 |
+
stem = "Krea2-{}".format(variant.capitalize()) if variant != "unknown" else "Krea2"
|
| 352 |
+
tag = "SVDQuant-W4A4-rank{}".format(rank) if rank else format.upper()
|
| 353 |
+
return os.path.join(ckpt_dir, "{}-AllInOne-{}-TE{}.safetensors".format(
|
| 354 |
+
stem, tag, te_format.upper()))
|
| 355 |
+
|
| 356 |
+
@classmethod
|
| 357 |
+
def VALIDATE_INPUTS(cls, source_dit, text_encoder, vae, output_name, overwrite,
|
| 358 |
+
variant, format, te_format, rank):
|
| 359 |
+
"""Same pre-flight as the sibling node, for the same reason.
|
| 360 |
+
|
| 361 |
+
This one writes ~12 GB and, from a BF16 source, spends ten minutes quantizing a DiT
|
| 362 |
+
before it reaches the combiner -- so "that file already exists" has to arrive at
|
| 363 |
+
Queue time rather than after the expensive part.
|
| 364 |
+
"""
|
| 365 |
+
for folder, name in (("diffusion_models", source_dit),
|
| 366 |
+
("text_encoders", text_encoder), ("vae", vae)):
|
| 367 |
+
try:
|
| 368 |
+
folder_paths.get_full_path_or_raise(folder, name)
|
| 369 |
+
except Exception as exc:
|
| 370 |
+
return str(exc)
|
| 371 |
+
try:
|
| 372 |
+
_, rank = resolve_format(format, rank, rank_was_set=False)
|
| 373 |
+
except Exception as exc:
|
| 374 |
+
return str(exc)
|
| 375 |
+
|
| 376 |
+
dst = cls._out_path(output_name, variant, format, rank, te_format)
|
| 377 |
+
if os.path.exists(dst) and not overwrite:
|
| 378 |
+
return ("{} already exists. Enable 'overwrite', or set a different output_name."
|
| 379 |
+
.format(dst))
|
| 380 |
+
free = _free_bytes(dst)
|
| 381 |
+
if free < _MIN_FREE_ALLINONE:
|
| 382 |
+
return ("only {:.1f} GB free on the drive holding {}; the combined checkpoint "
|
| 383 |
+
"needs roughly {:.0f} GB.".format(free / 1024 ** 3, os.path.dirname(dst),
|
| 384 |
+
_MIN_FREE_ALLINONE / 1024 ** 3))
|
| 385 |
+
return True
|
| 386 |
+
|
| 387 |
+
@classmethod
|
| 388 |
+
def IS_CHANGED(cls, *args, **kwargs):
|
| 389 |
+
# ~12 GB written as a side effect. Same reasoning as the sibling node: a cached
|
| 390 |
+
# summary claims a file exists that the user may since have deleted.
|
| 391 |
+
return float("nan")
|
| 392 |
+
|
| 393 |
+
RETURN_TYPES = ("STRING",)
|
| 394 |
+
RETURN_NAMES = ("summary",)
|
| 395 |
+
OUTPUT_TOOLTIPS = ("Size, tensor counts and loader instructions for the baked checkpoint.",)
|
| 396 |
+
OUTPUT_NODE = True
|
| 397 |
+
FUNCTION = "run"
|
| 398 |
+
CATEGORY = _CATEGORY
|
| 399 |
+
TITLE = "Krea2 SVDQuant Quantize All-in-One"
|
| 400 |
+
DESCRIPTION = ("Bakes DiT, 4-bit quantized Qwen3-VL 4B text encoder, and VAE into a single "
|
| 401 |
+
"combined checkpoint in ComfyUI/models/checkpoints/ (~12 GB instead of 15.5 GB "
|
| 402 |
+
"across three files).")
|
| 403 |
+
|
| 404 |
+
def run(self, source_dit, text_encoder, vae, format, te_format, rank, refine_iters,
|
| 405 |
+
groupsize, variant, output_name, overwrite, act_stats="", seed=0):
|
| 406 |
+
from safetensors import safe_open
|
| 407 |
+
# `.build_all_in_one`, not `.tools.build_all_in_one`: `.comfyignore` keeps `tools/`
|
| 408 |
+
# out of the published package, so the old import raised ImportError for every
|
| 409 |
+
# Registry/Manager install and worked only for a git clone.
|
| 410 |
+
from .build_all_in_one import build_all_in_one_checkpoint
|
| 411 |
+
|
| 412 |
+
dit_src = folder_paths.get_full_path_or_raise("diffusion_models", source_dit)
|
| 413 |
+
te_src = folder_paths.get_full_path_or_raise("text_encoders", text_encoder)
|
| 414 |
+
vae_src = folder_paths.get_full_path_or_raise("vae", vae)
|
| 415 |
+
|
| 416 |
+
fmt, rank = resolve_format(format, rank, rank_was_set=False)
|
| 417 |
+
|
| 418 |
+
os.makedirs(folder_paths.get_folder_paths("checkpoints")[0], exist_ok=True)
|
| 419 |
+
dst = self._out_path(output_name, variant, format, rank, te_format)
|
| 420 |
+
|
| 421 |
+
if os.path.exists(dst) and not overwrite:
|
| 422 |
+
raise RuntimeError(
|
| 423 |
+
"{} already exists. Enable 'overwrite', or set a different output_name."
|
| 424 |
+
.format(dst))
|
| 425 |
+
|
| 426 |
+
free = _free_bytes(dst)
|
| 427 |
+
if free < _MIN_FREE_ALLINONE:
|
| 428 |
+
raise RuntimeError(
|
| 429 |
+
"only {:.1f} GB free on the drive holding {}; baking all-in-one needs "
|
| 430 |
+
"roughly {:.0f} GB headroom.".format(
|
| 431 |
+
free / 1024 ** 3, dst, _MIN_FREE_ALLINONE / 1024 ** 3))
|
| 432 |
+
|
| 433 |
+
comfy.model_management.unload_all_models()
|
| 434 |
+
comfy.model_management.soft_empty_cache()
|
| 435 |
+
|
| 436 |
+
# Check if DiT is already quantized or needs quantization
|
| 437 |
+
with safe_open(dit_src, framework="pt", device="cpu") as handle:
|
| 438 |
+
keys = list(handle.keys())
|
| 439 |
+
already_quantized = any(k.endswith(".comfy_quant") for k in keys)
|
| 440 |
+
temp_dit = None
|
| 441 |
+
|
| 442 |
+
if not already_quantized:
|
| 443 |
+
stats_path = act_stats.strip() or None
|
| 444 |
+
if stats_path is not None and not os.path.isabs(stats_path):
|
| 445 |
+
stats_path = os.path.join(folder_paths.get_output_directory(), stats_path)
|
| 446 |
+
# ComfyUI's temp directory rather than models/checkpoints/: a run killed
|
| 447 |
+
# mid-quantize leaves this ~8 GB file behind -- the `finally` below only covers
|
| 448 |
+
# a clean unwind -- and beside the real checkpoints it shows up in every loader
|
| 449 |
+
# dropdown as something that looks loadable and is not.
|
| 450 |
+
temp_dir = folder_paths.get_temp_directory()
|
| 451 |
+
os.makedirs(temp_dir, exist_ok=True)
|
| 452 |
+
temp_dit = os.path.join(temp_dir,
|
| 453 |
+
os.path.basename(dst) + ".tmp_dit.safetensors")
|
| 454 |
+
temp_free = _free_bytes(temp_dit)
|
| 455 |
+
if temp_free < _MIN_FREE_INTERMEDIATE:
|
| 456 |
+
raise RuntimeError(
|
| 457 |
+
"only {:.1f} GB free on the drive holding {}; quantizing the DiT first "
|
| 458 |
+
"writes an intermediate of roughly {:.0f} GB there.".format(
|
| 459 |
+
temp_free / 1024 ** 3, temp_dir,
|
| 460 |
+
_MIN_FREE_INTERMEDIATE / 1024 ** 3))
|
| 461 |
+
logging.info("[krea2-svdquant] quantizing DiT first: %s -> %s", dit_src, temp_dit)
|
| 462 |
+
convert(dit_src, temp_dit, fmt, groupsize, "cuda", rank, refine_iters,
|
| 463 |
+
variant=variant, rank_alloc="uniform", act_stats=stats_path,
|
| 464 |
+
seed=None if seed < 0 else int(seed))
|
| 465 |
+
active_dit = temp_dit
|
| 466 |
+
else:
|
| 467 |
+
active_dit = dit_src
|
| 468 |
+
|
| 469 |
+
pbar = comfy.utils.ProgressBar(1)
|
| 470 |
+
state = {"total": 0}
|
| 471 |
+
|
| 472 |
+
def progress(done, total, message):
|
| 473 |
+
if total != state["total"]:
|
| 474 |
+
state["total"] = total
|
| 475 |
+
pbar.total = total
|
| 476 |
+
pbar.update_absolute(done, total)
|
| 477 |
+
|
| 478 |
+
try:
|
| 479 |
+
summary = build_all_in_one_checkpoint(
|
| 480 |
+
dit_path=active_dit,
|
| 481 |
+
text_encoder_path=te_src,
|
| 482 |
+
vae_path=vae_src,
|
| 483 |
+
out_path=dst,
|
| 484 |
+
fmt_name=format,
|
| 485 |
+
te_format=te_format,
|
| 486 |
+
rank=rank,
|
| 487 |
+
variant=variant,
|
| 488 |
+
groupsize=groupsize,
|
| 489 |
+
device="cuda",
|
| 490 |
+
progress_cb=progress,
|
| 491 |
+
)
|
| 492 |
+
finally:
|
| 493 |
+
if temp_dit and os.path.exists(temp_dit):
|
| 494 |
+
try:
|
| 495 |
+
os.remove(temp_dit)
|
| 496 |
+
except Exception:
|
| 497 |
+
# Not worth failing the run over -- the checkpoint is already written --
|
| 498 |
+
# but ~8 GB staying behind in the temp directory is worth one line,
|
| 499 |
+
# because nothing else will ever mention it.
|
| 500 |
+
logging.warning(
|
| 501 |
+
"[krea2-svdquant] could not delete the intermediate %s; roughly "
|
| 502 |
+
"8 GB is still there", temp_dit, exc_info=True)
|
| 503 |
+
|
| 504 |
+
hint = SAMPLER_HINTS.get(variant)
|
| 505 |
+
loader = ("Krea2 SVDQuant Checkpoint Loader" if rank else "CheckpointLoaderSimple")
|
| 506 |
+
text = "\n".join(x for x in (
|
| 507 |
+
summary, "Load it with {}.".format(loader), hint) if x)
|
| 508 |
+
logging.info("[krea2-svdquant] %s", text)
|
| 509 |
+
return {"ui": {"text": [text]}, "result": (text,)}
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
NODE_CLASS_MAPPINGS = {
|
| 513 |
+
"Krea2SVDQuantQuantize": Krea2SVDQuantQuantize,
|
| 514 |
+
"Krea2SVDQuantQuantizeAllInOne": Krea2SVDQuantQuantizeAllInOne,
|
| 515 |
+
}
|
| 516 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 517 |
+
"Krea2SVDQuantQuantize": "Krea2 SVDQuant Quantize",
|
| 518 |
+
"Krea2SVDQuantQuantizeAllInOne": "Krea2 SVDQuant Quantize All-in-One",
|
| 519 |
+
}
|
svdquant_w4a4.py
CHANGED
|
@@ -33,6 +33,64 @@ from .svdquant_diag import BUF_L1, BUF_L2, _CATEGORY, branch_factors, log_dispat
|
|
| 33 |
_L1 = "." + BUF_L1
|
| 34 |
_L2 = "." + BUF_L2
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
def has_branch(module: torch.nn.Module) -> bool:
|
| 38 |
"""True once this module carries a low-rank branch (i.e. it is a quantized linear)."""
|
|
@@ -61,9 +119,10 @@ def _publish_in_state_dict(module: torch.nn.Module) -> None:
|
|
| 61 |
emits weight/scale/marker/bias and nothing else -- buffers never reach it. That
|
| 62 |
matters because ``model_management.module_size()`` sums ``state_dict()``, and every
|
| 63 |
per-module VRAM decision ComfyUI makes (the lowvram split in particular) is derived
|
| 64 |
-
from that number. Left alone,
|
| 65 |
-
|
| 66 |
-
8 GB card OOMs on this checkpoint but
|
|
|
|
| 67 |
|
| 68 |
Emitting them under their own names also means a model saved out of ComfyUI carries
|
| 69 |
the same ``<layer>.svdq_l1`` keys quantize_krea2.py writes, so it round-trips.
|
|
@@ -97,9 +156,9 @@ def attach_branch(module: torch.nn.Module, l1: torch.Tensor, l2: torch.Tensor,
|
|
| 97 |
by `_publish_in_state_dict` -- on these modules persistence alone is not enough, see
|
| 98 |
that function for why. Both steps are needed to get them counted by
|
| 99 |
``model_management.module_size()``, which is what every VRAM decision (full load, the
|
| 100 |
-
lowvram split, how much to free) is derived from. Uncounted, they are
|
| 101 |
-
|
| 102 |
-
on an 8 GB card.
|
| 103 |
|
| 104 |
`scale` is folded into l2 at attach time rather than applied to the output every step:
|
| 105 |
l2 is [rank, in_features] while the output is [tokens, out_features], so this is about
|
|
@@ -339,39 +398,25 @@ def _install_custom_op(module: torch.nn.Module) -> bool:
|
|
| 339 |
return True
|
| 340 |
|
| 341 |
|
| 342 |
-
def
|
| 343 |
-
|
| 344 |
-
sd, metadata = comfy.utils.load_torch_file(path, return_metadata=True)
|
| 345 |
-
# `comfy.sd.load_diffusion_model_state_dict` strips this prefix internally when it builds
|
| 346 |
-
# the module tree, but we walk that tree ourselves to attach branches, so we strip it too.
|
| 347 |
-
# `default=""` rather than raising: a loader must not blow up on a checkpoint with no
|
| 348 |
-
# blocks, and the "found no branches" error below says far more about what went wrong.
|
| 349 |
-
layer_prefix = detect_prefix(sd.keys(), default="")
|
| 350 |
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
"
|
| 360 |
-
|
| 361 |
-
)
|
| 362 |
|
| 363 |
-
# `disable_dynamic=True` pins this to the classic ModelPatcher. On current ComfyUI
|
| 364 |
-
# that is a no-op (`CoreModelPatcher is ModelPatcher`), but when upstream flips it to
|
| 365 |
-
# ModelPatcherDynamic the streaming patcher takes ownership of the weights via
|
| 366 |
-
# `load_model_weights(..., assign=patcher.is_dynamic())`, which we have not validated
|
| 367 |
-
# against the branch buffers. Keep it pinned until that path is tested; the
|
| 368 |
-
# diagnostics node reports which patcher is actually in use.
|
| 369 |
-
patcher = comfy.sd.load_diffusion_model_state_dict(
|
| 370 |
-
sd, model_options=model_options or {}, metadata=metadata, disable_dynamic=True
|
| 371 |
-
)
|
| 372 |
-
if patcher is None:
|
| 373 |
-
raise RuntimeError("could not detect a model in {}".format(path))
|
| 374 |
|
|
|
|
|
|
|
|
|
|
| 375 |
diffusion_model = patcher.model.diffusion_model
|
| 376 |
model_dtype = patcher.model.get_dtype()
|
| 377 |
attached = 0
|
|
@@ -453,9 +498,9 @@ def load_svdquant_w4a4(path: str, model_options: dict | None = None,
|
|
| 453 |
compile_desc = "compile: all {} layers are graph breaks{}".format(
|
| 454 |
attached, " ({})".format(_W4A4_OP_ERROR) if _W4A4_OP_ERROR else "")
|
| 455 |
summary = ("w4a4 + low-rank: attached {} branches (rank {}, variant {}), "
|
| 456 |
-
"model_size {:.2f} GiB, {}".format(
|
| 457 |
attached, rank_desc, variant, patcher.model_size() / 1024 ** 3,
|
| 458 |
-
compile_desc))
|
| 459 |
logging.info("[krea2-svdquant] %s", summary)
|
| 460 |
|
| 461 |
dispatch = log_dispatch(diffusion_model)
|
|
@@ -465,9 +510,95 @@ def load_svdquant_w4a4(path: str, model_options: dict | None = None,
|
|
| 465 |
# matters most for the dispatch warning: buried in the console, the people who most need
|
| 466 |
# to read it are exactly the ones who never see it.
|
| 467 |
patcher.krea2_load_summary = "\n".join(x for x in (summary, dispatch) if x)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
return patcher
|
| 469 |
|
| 470 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
class Krea2SVDQuantW4A4Loader:
|
| 472 |
@classmethod
|
| 473 |
def INPUT_TYPES(cls):
|
|
@@ -479,7 +610,16 @@ class Krea2SVDQuantW4A4Loader:
|
|
| 479 |
"checkpoints have no branch and load with the stock UNETLoader "
|
| 480 |
"instead.",
|
| 481 |
}),
|
| 482 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
}
|
| 484 |
|
| 485 |
RETURN_TYPES = ("MODEL", "STRING")
|
|
@@ -495,12 +635,59 @@ class Krea2SVDQuantW4A4Loader:
|
|
| 495 |
"no separate base model needed. The status output tells you whether the "
|
| 496 |
"fast int4 kernel is in play.")
|
| 497 |
|
| 498 |
-
def load(self, model_name):
|
| 499 |
path = folder_paths.get_full_path_or_raise("diffusion_models", model_name)
|
| 500 |
-
patcher = load_svdquant_w4a4(path)
|
| 501 |
status = getattr(patcher, "krea2_load_summary", "")
|
| 502 |
return {"ui": {"text": [status]}, "result": (patcher, status)}
|
| 503 |
|
| 504 |
|
| 505 |
-
|
| 506 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
_L1 = "." + BUF_L1
|
| 34 |
_L2 = "." + BUF_L2
|
| 35 |
|
| 36 |
+
# How the two loaders answer `disable_dynamic=` when they build the patcher.
|
| 37 |
+
VRAM_MANAGEMENT = ["auto", "classic"]
|
| 38 |
+
|
| 39 |
+
# Both loaders offer the same choice, so the wording lives in one place. Long, because the
|
| 40 |
+
# question it answers -- "why is this suddenly 30 s an iteration" -- is one nobody arrives
|
| 41 |
+
# at the tooltip already understanding.
|
| 42 |
+
VRAM_TOOLTIP = (
|
| 43 |
+
'Leave on "auto" unless something is wrong. "auto" lets ComfyUI manage this model the '
|
| 44 |
+
"way it manages any other, which on an ordinary launch means the dynamic-VRAM "
|
| 45 |
+
"streaming patcher -- that is what keeps iteration time flat once the model stops "
|
| 46 |
+
'fitting in VRAM. "classic" pins it to the older patcher, where a model that no longer '
|
| 47 |
+
"fits falls back to streaming weights per module per step and an iteration goes from "
|
| 48 |
+
"~1 s to 30-100 s. KREA2_DISABLE_DYNAMIC=1 forces classic for a whole server run."
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _disable_dynamic(mode: str = "auto") -> bool:
|
| 53 |
+
"""Whether to pin this model to the classic `ModelPatcher`.
|
| 54 |
+
|
| 55 |
+
`auto` means "whatever ComfyUI would do for a stock UNETLoader", which since the
|
| 56 |
+
dynamic-VRAM work means the streaming `ModelPatcherDynamic` on any ordinary launch
|
| 57 |
+
(`main.py` swaps `CoreModelPatcher` for it unless --highvram/--novram/--gpu-only/--cpu
|
| 58 |
+
or --disable-dynamic-vram is set). This loader used to hardcode the pin, and that is
|
| 59 |
+
the whole reason an svdq checkpoint fell to per-module weight streaming past the point
|
| 60 |
+
where it stopped fitting -- 30-100 s an iteration -- while the same-size FP8/INT8/
|
| 61 |
+
noLowRank files, which go through the stock loader, did not.
|
| 62 |
+
|
| 63 |
+
Both conditions the pin was waiting on hold on current ComfyUI, and both were
|
| 64 |
+
measured rather than argued from the source:
|
| 65 |
+
|
| 66 |
+
* the branch buffers survive the streaming patcher. Its `load()` walks
|
| 67 |
+
`named_buffers()`, moves every one to the load device and backs up the original.
|
| 68 |
+
Read off the diagnostics node after a render, under enough pressure to force
|
| 69 |
+
offloading: all 448 factors on `cuda:0` while all 224 quantized weights sit on `cpu`
|
| 70 |
+
and stream. So `add_low_rank`'s `cast_to` is a no-op, and the split is the one you
|
| 71 |
+
would choose -- the small branch every step needs stays resident, the big weights do
|
| 72 |
+
not. The same reading under `classic` is 368 factors on `cpu`, i.e. staged per call.
|
| 73 |
+
* `module_size()` still counts them, because `_load_list` derives its per-module
|
| 74 |
+
budget from `model_management.module_size`, which sums `state_dict()` -- exactly
|
| 75 |
+
what `_publish_in_state_dict` exists to feed.
|
| 76 |
+
|
| 77 |
+
`classic` restores the old behaviour. It is kept because one thing here *is*
|
| 78 |
+
unmeasured: the streaming patcher moves those buffers with a plain `.to()` rather than
|
| 79 |
+
through its own allocator, so 1.6 GiB at rank 256 is real VRAM its `allocated_size`
|
| 80 |
+
bookkeeping does not see. If that turns out to matter on some card, the way back should
|
| 81 |
+
be a dropdown, not a downgrade.
|
| 82 |
+
|
| 83 |
+
The env var is the same escape hatch `KREA2_W4A4_OP=0` is, and for the same reason:
|
| 84 |
+
the choice is made once per load, so A/B-ing it means two server runs that differ by
|
| 85 |
+
exactly this.
|
| 86 |
+
"""
|
| 87 |
+
if mode == "classic":
|
| 88 |
+
return True
|
| 89 |
+
if mode != "auto":
|
| 90 |
+
raise ValueError("vram_management must be one of {}, got {!r}".format(
|
| 91 |
+
VRAM_MANAGEMENT, mode))
|
| 92 |
+
return os.environ.get("KREA2_DISABLE_DYNAMIC") == "1"
|
| 93 |
+
|
| 94 |
|
| 95 |
def has_branch(module: torch.nn.Module) -> bool:
|
| 96 |
"""True once this module carries a low-rank branch (i.e. it is a quantized linear)."""
|
|
|
|
| 119 |
emits weight/scale/marker/bias and nothing else -- buffers never reach it. That
|
| 120 |
matters because ``model_management.module_size()`` sums ``state_dict()``, and every
|
| 121 |
per-module VRAM decision ComfyUI makes (the lowvram split in particular) is derived
|
| 122 |
+
from that number. Left alone, 3.75-11 MiB per layer and 1.6 GiB across the 224 blocks
|
| 123 |
+
(measured off a rank-256 file; 0.4 GiB at rank 64) would sit on the GPU while the budget
|
| 124 |
+
believed it was free -- which is exactly how an 8 GB card OOMs on this checkpoint but
|
| 125 |
+
not on the branch-free int8 one.
|
| 126 |
|
| 127 |
Emitting them under their own names also means a model saved out of ComfyUI carries
|
| 128 |
the same ``<layer>.svdq_l1`` keys quantize_krea2.py writes, so it round-trips.
|
|
|
|
| 156 |
by `_publish_in_state_dict` -- on these modules persistence alone is not enough, see
|
| 157 |
that function for why. Both steps are needed to get them counted by
|
| 158 |
``model_management.module_size()``, which is what every VRAM decision (full load, the
|
| 159 |
+
lowvram split, how much to free) is derived from. Uncounted, they are 3.75-11 MiB per
|
| 160 |
+
layer and 1.6 GiB across the 224 blocks at rank 256 (0.4 GiB at rank 64): the difference
|
| 161 |
+
between fitting and OOMing on an 8 GB card.
|
| 162 |
|
| 163 |
`scale` is folded into l2 at attach time rather than applied to the output every step:
|
| 164 |
l2 is [rank, in_features] while the output is [tokens, out_features], so this is about
|
|
|
|
| 398 |
return True
|
| 399 |
|
| 400 |
|
| 401 |
+
def _patcher_desc(patcher) -> str:
|
| 402 |
+
"""Which patcher this model got, for the load summary.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
| 404 |
+
Worth stating on every load rather than only when something goes wrong: "dynamic" and
|
| 405 |
+
"pinned" are the difference between a 1 s iteration and a 30 s one once the model stops
|
| 406 |
+
fitting, and the status output is the one place a user is told to look when generation
|
| 407 |
+
is slow. `is_dynamic` is wrapped because it is a newer ComfyUI API and a loader must not
|
| 408 |
+
fail to load a checkpoint over a status string.
|
| 409 |
+
"""
|
| 410 |
+
try:
|
| 411 |
+
return "{} (dynamic vram: {})".format(
|
| 412 |
+
type(patcher).__name__, "on" if patcher.is_dynamic() else "off")
|
| 413 |
+
except Exception:
|
| 414 |
+
return type(patcher).__name__
|
| 415 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
+
def _attach_svdq_branches(patcher, branches: dict[str, dict[str, torch.Tensor]],
|
| 418 |
+
layer_prefix: str, metadata: dict | None, path: str,
|
| 419 |
+
compile_safe: bool = True) -> str:
|
| 420 |
diffusion_model = patcher.model.diffusion_model
|
| 421 |
model_dtype = patcher.model.get_dtype()
|
| 422 |
attached = 0
|
|
|
|
| 498 |
compile_desc = "compile: all {} layers are graph breaks{}".format(
|
| 499 |
attached, " ({})".format(_W4A4_OP_ERROR) if _W4A4_OP_ERROR else "")
|
| 500 |
summary = ("w4a4 + low-rank: attached {} branches (rank {}, variant {}), "
|
| 501 |
+
"model_size {:.2f} GiB, {}, {}".format(
|
| 502 |
attached, rank_desc, variant, patcher.model_size() / 1024 ** 3,
|
| 503 |
+
_patcher_desc(patcher), compile_desc))
|
| 504 |
logging.info("[krea2-svdquant] %s", summary)
|
| 505 |
|
| 506 |
dispatch = log_dispatch(diffusion_model)
|
|
|
|
| 510 |
# matters most for the dispatch warning: buried in the console, the people who most need
|
| 511 |
# to read it are exactly the ones who never see it.
|
| 512 |
patcher.krea2_load_summary = "\n".join(x for x in (summary, dispatch) if x)
|
| 513 |
+
return patcher.krea2_load_summary
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
def load_svdquant_w4a4(path: str, model_options: dict | None = None,
|
| 517 |
+
compile_safe: bool = True, vram_management: str = "auto"):
|
| 518 |
+
sd, metadata = comfy.utils.load_torch_file(path, return_metadata=True)
|
| 519 |
+
# `comfy.sd.load_diffusion_model_state_dict` strips this prefix internally when it builds
|
| 520 |
+
# the module tree, but we walk that tree ourselves to attach branches, so we strip it too.
|
| 521 |
+
# `default=""` rather than raising: a loader must not blow up on a checkpoint with no
|
| 522 |
+
# blocks, and the "found no branches" error below says far more about what went wrong.
|
| 523 |
+
layer_prefix = detect_prefix(sd.keys(), default="")
|
| 524 |
+
|
| 525 |
+
branches: dict[str, dict[str, torch.Tensor]] = {}
|
| 526 |
+
for key in list(sd.keys()):
|
| 527 |
+
for suffix, slot in ((_L1, "l1"), (_L2, "l2")):
|
| 528 |
+
if key.endswith(suffix):
|
| 529 |
+
branches.setdefault(key[: -len(suffix)], {})[slot] = sd.pop(key)
|
| 530 |
+
break
|
| 531 |
+
if not branches:
|
| 532 |
+
raise ValueError(
|
| 533 |
+
"{} carries no svdq_l1/svdq_l2 tensors - it is a plain quantized checkpoint, "
|
| 534 |
+
"load it with UNETLoader instead".format(path)
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
# See `_disable_dynamic` for why this is no longer hardcoded to True. The diagnostics
|
| 538 |
+
# node and the load summary both report which patcher the model actually got.
|
| 539 |
+
patcher = comfy.sd.load_diffusion_model_state_dict(
|
| 540 |
+
sd, model_options=model_options or {}, metadata=metadata,
|
| 541 |
+
disable_dynamic=_disable_dynamic(vram_management)
|
| 542 |
+
)
|
| 543 |
+
if patcher is None:
|
| 544 |
+
raise RuntimeError("could not detect a model in {}".format(path))
|
| 545 |
+
|
| 546 |
+
_attach_svdq_branches(patcher, branches, layer_prefix, metadata, path, compile_safe)
|
| 547 |
return patcher
|
| 548 |
|
| 549 |
|
| 550 |
+
def load_svdquant_checkpoint(path: str, output_vae: bool = True, output_clip: bool = True,
|
| 551 |
+
model_options: dict | None = None,
|
| 552 |
+
te_model_options: dict | None = None,
|
| 553 |
+
compile_safe: bool = True, vram_management: str = "auto"):
|
| 554 |
+
"""Load an all-in-one Krea 2 checkpoint (diffusion model + text encoder + VAE).
|
| 555 |
+
|
| 556 |
+
If the checkpoint carries SVDQuant branches (model.diffusion_model.blocks.N.*.svdq_l1/l2),
|
| 557 |
+
they are extracted, the model is built via ComfyUI's load_state_dict_guess_config, and
|
| 558 |
+
the low-rank branches are attached. If the checkpoint is branchless (e.g. w4a4 or int8),
|
| 559 |
+
it is loaded cleanly without branches.
|
| 560 |
+
"""
|
| 561 |
+
sd, metadata = comfy.utils.load_torch_file(path, return_metadata=True)
|
| 562 |
+
|
| 563 |
+
branches: dict[str, dict[str, torch.Tensor]] = {}
|
| 564 |
+
for key in list(sd.keys()):
|
| 565 |
+
for suffix, slot in ((_L1, "l1"), (_L2, "l2")):
|
| 566 |
+
if key.endswith(suffix):
|
| 567 |
+
branches.setdefault(key[: -len(suffix)], {})[slot] = sd.pop(key)
|
| 568 |
+
break
|
| 569 |
+
|
| 570 |
+
out = comfy.sd.load_state_dict_guess_config(
|
| 571 |
+
sd,
|
| 572 |
+
output_vae=output_vae,
|
| 573 |
+
output_clip=output_clip,
|
| 574 |
+
output_clipvision=False,
|
| 575 |
+
embedding_directory=folder_paths.get_folder_paths("embeddings"),
|
| 576 |
+
model_options=model_options or {},
|
| 577 |
+
te_model_options=te_model_options or {},
|
| 578 |
+
metadata=metadata,
|
| 579 |
+
disable_dynamic=_disable_dynamic(vram_management),
|
| 580 |
+
)
|
| 581 |
+
if out is None:
|
| 582 |
+
raise RuntimeError("could not detect a valid checkpoint in {}".format(path))
|
| 583 |
+
|
| 584 |
+
patcher, clip, vae, _ = out
|
| 585 |
+
|
| 586 |
+
if branches:
|
| 587 |
+
layer_prefix = detect_prefix(branches.keys(), default="")
|
| 588 |
+
_attach_svdq_branches(patcher, branches, layer_prefix, metadata, path, compile_safe)
|
| 589 |
+
else:
|
| 590 |
+
diffusion_model = patcher.model.diffusion_model
|
| 591 |
+
install_mask_guard(patcher)
|
| 592 |
+
dispatch = log_dispatch(diffusion_model)
|
| 593 |
+
summary = "all-in-one checkpoint (branchless): model_size {:.2f} GiB, {}".format(
|
| 594 |
+
patcher.model_size() / 1024 ** 3, _patcher_desc(patcher))
|
| 595 |
+
logging.info("[krea2-svdquant] %s", summary)
|
| 596 |
+
patcher.krea2_load_summary = "\n".join(x for x in (summary, dispatch) if x)
|
| 597 |
+
|
| 598 |
+
status = getattr(patcher, "krea2_load_summary", "")
|
| 599 |
+
return patcher, clip, vae, status
|
| 600 |
+
|
| 601 |
+
|
| 602 |
class Krea2SVDQuantW4A4Loader:
|
| 603 |
@classmethod
|
| 604 |
def INPUT_TYPES(cls):
|
|
|
|
| 610 |
"checkpoints have no branch and load with the stock UNETLoader "
|
| 611 |
"instead.",
|
| 612 |
}),
|
| 613 |
+
},
|
| 614 |
+
# Optional and appended last: ComfyUI matches `widgets_values` positionally, so
|
| 615 |
+
# inserting a widget anywhere else shifts every value in a workflow saved
|
| 616 |
+
# before this input existed.
|
| 617 |
+
"optional": {
|
| 618 |
+
"vram_management": (VRAM_MANAGEMENT, {
|
| 619 |
+
"default": "auto",
|
| 620 |
+
"tooltip": VRAM_TOOLTIP,
|
| 621 |
+
}),
|
| 622 |
+
},
|
| 623 |
}
|
| 624 |
|
| 625 |
RETURN_TYPES = ("MODEL", "STRING")
|
|
|
|
| 635 |
"no separate base model needed. The status output tells you whether the "
|
| 636 |
"fast int4 kernel is in play.")
|
| 637 |
|
| 638 |
+
def load(self, model_name, vram_management="auto"):
|
| 639 |
path = folder_paths.get_full_path_or_raise("diffusion_models", model_name)
|
| 640 |
+
patcher = load_svdquant_w4a4(path, vram_management=vram_management)
|
| 641 |
status = getattr(patcher, "krea2_load_summary", "")
|
| 642 |
return {"ui": {"text": [status]}, "result": (patcher, status)}
|
| 643 |
|
| 644 |
|
| 645 |
+
class Krea2SVDQuantCheckpointLoader:
|
| 646 |
+
@classmethod
|
| 647 |
+
def INPUT_TYPES(cls):
|
| 648 |
+
return {
|
| 649 |
+
"required": {
|
| 650 |
+
"ckpt_name": (folder_paths.get_filename_list("checkpoints"), {
|
| 651 |
+
"tooltip": "An all-in-one Krea2 checkpoint containing diffusion model, "
|
| 652 |
+
"quantized text encoder, and VAE. If it carries SVDQuant "
|
| 653 |
+
"branches (*.svdq_l1/*.svdq_l2), they will be attached automatically.",
|
| 654 |
+
}),
|
| 655 |
+
},
|
| 656 |
+
# See the sibling loader: appended last, for the same positional reason.
|
| 657 |
+
"optional": {
|
| 658 |
+
"vram_management": (VRAM_MANAGEMENT, {
|
| 659 |
+
"default": "auto",
|
| 660 |
+
"tooltip": VRAM_TOOLTIP,
|
| 661 |
+
}),
|
| 662 |
+
},
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
RETURN_TYPES = ("MODEL", "CLIP", "VAE", "STRING")
|
| 666 |
+
RETURN_NAMES = ("model", "clip", "vae", "status")
|
| 667 |
+
OUTPUT_TOOLTIPS = ("Wire this to a KSampler.",
|
| 668 |
+
"Wire this to CLIPTextEncode.",
|
| 669 |
+
"Wire this to VAEDecode.",
|
| 670 |
+
"Rank, variant, size and which kernel the quantized layers will "
|
| 671 |
+
"actually dispatch to.")
|
| 672 |
+
OUTPUT_NODE = True
|
| 673 |
+
FUNCTION = "load"
|
| 674 |
+
CATEGORY = _CATEGORY
|
| 675 |
+
TITLE = "Krea2 SVDQuant Checkpoint Loader"
|
| 676 |
+
DESCRIPTION = ("Loads an all-in-one Krea2 checkpoint containing diffusion model, text encoder, "
|
| 677 |
+
"and VAE in a single file. Attaches low-rank SVDQuant branches if present.")
|
| 678 |
+
|
| 679 |
+
def load(self, ckpt_name, vram_management="auto"):
|
| 680 |
+
path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)
|
| 681 |
+
patcher, clip, vae, status = load_svdquant_checkpoint(
|
| 682 |
+
path, vram_management=vram_management)
|
| 683 |
+
return {"ui": {"text": [status]}, "result": (patcher, clip, vae, status)}
|
| 684 |
+
|
| 685 |
+
|
| 686 |
+
NODE_CLASS_MAPPINGS = {
|
| 687 |
+
"Krea2SVDQuantW4A4Loader": Krea2SVDQuantW4A4Loader,
|
| 688 |
+
"Krea2SVDQuantCheckpointLoader": Krea2SVDQuantCheckpointLoader,
|
| 689 |
+
}
|
| 690 |
+
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 691 |
+
"Krea2SVDQuantW4A4Loader": "Krea2 SVDQuant W4A4 Loader",
|
| 692 |
+
"Krea2SVDQuantCheckpointLoader": "Krea2 SVDQuant Checkpoint Loader",
|
| 693 |
+
}
|
tools/bake_adapter.py
CHANGED
|
@@ -28,14 +28,15 @@ import sys
|
|
| 28 |
|
| 29 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 30 |
sys.path.insert(0, os.path.dirname(HERE)) # the node package
|
| 31 |
-
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(HERE)))) # ComfyUI root
|
| 32 |
|
| 33 |
import torch # noqa: E402
|
| 34 |
|
| 35 |
-
|
| 36 |
-
import
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
| 39 |
from quantize_krea2 import ( # noqa: E402
|
| 40 |
LAYER_PREFIXES,
|
| 41 |
convert,
|
|
@@ -44,6 +45,10 @@ from quantize_krea2 import ( # noqa: E402
|
|
| 44 |
resolve_format,
|
| 45 |
)
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
_PREFIX = "diffusion_model."
|
| 48 |
_ALT_PREFIX = "transformer."
|
| 49 |
|
|
|
|
| 28 |
|
| 29 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 30 |
sys.path.insert(0, os.path.dirname(HERE)) # the node package
|
|
|
|
| 31 |
|
| 32 |
import torch # noqa: E402
|
| 33 |
|
| 34 |
+
# `quantize_krea2` before `comfy.*`, and that order is the point. It runs
|
| 35 |
+
# `_find_comfyui_root()` at import -- $COMFYUI_PATH, then two levels up from itself, then a
|
| 36 |
+
# couple of fallbacks -- so importing it is what puts ComfyUI on the path. This file used to
|
| 37 |
+
# do that itself with a bare `dirname(dirname(dirname(HERE)))`, which is correct only when
|
| 38 |
+
# the pack sits in `ComfyUI/custom_nodes/`: from a working copy anywhere else, even `--help`
|
| 39 |
+
# died on `No module named 'comfy'` with nothing to say about why.
|
| 40 |
from quantize_krea2 import ( # noqa: E402
|
| 41 |
LAYER_PREFIXES,
|
| 42 |
convert,
|
|
|
|
| 45 |
resolve_format,
|
| 46 |
)
|
| 47 |
|
| 48 |
+
import comfy.lora # noqa: E402
|
| 49 |
+
import comfy.utils # noqa: E402
|
| 50 |
+
import comfy.weight_adapter # noqa: E402
|
| 51 |
+
|
| 52 |
_PREFIX = "diffusion_model."
|
| 53 |
_ALT_PREFIX = "transformer."
|
| 54 |
|
tools/build_all_in_one.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Command-line front end for `build_all_in_one.py`.
|
| 2 |
+
|
| 3 |
+
The mechanism lives one directory up, beside `quantize_krea2.py`, because `.comfyignore`
|
| 4 |
+
keeps `tools/` out of the published package and the in-graph quantize node imports it.
|
| 5 |
+
This file is the part a terminal needs: the sys.path bootstrap that makes the package
|
| 6 |
+
importable when it is run as a plain script, and the argument parsing.
|
| 7 |
+
|
| 8 |
+
python tools/build_all_in_one.py --dit ... --text-encoder ... --vae ... --format svdq
|
| 9 |
+
|
| 10 |
+
Run `--help` for the full set. `--dry-run` does the whole key mapping and prints the size
|
| 11 |
+
arithmetic without quantizing anything or touching the GPU.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
|
| 19 |
+
# The package directory, so `import build_all_in_one` resolves to the module beside
|
| 20 |
+
# `quantize_krea2.py`. It in turn finds ComfyUI itself (`quantize_krea2._find_comfyui_root`),
|
| 21 |
+
# so there is nothing else to bootstrap here.
|
| 22 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 23 |
+
|
| 24 |
+
from build_all_in_one import TE_FORMATS, build_all_in_one_checkpoint # noqa: E402
|
| 25 |
+
from quantize_krea2 import DEFAULT_SEED, REFINE_TOL, resolve_format # noqa: E402
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def main():
|
| 29 |
+
ap = argparse.ArgumentParser(description=__doc__,
|
| 30 |
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 31 |
+
ap.add_argument("--dit", required=True,
|
| 32 |
+
help="the diffusion model: a BF16 source, or one this tool already "
|
| 33 |
+
"quantized (detected, and passed through untouched)")
|
| 34 |
+
ap.add_argument("--text-encoder", required=True, help="qwen3vl_4b BF16 safetensors")
|
| 35 |
+
ap.add_argument("--vae", required=True)
|
| 36 |
+
ap.add_argument("--format", choices=["int8", "w4a4", "svdq", "fp8"], default="svdq",
|
| 37 |
+
help="diffusion side. svdq needs this repo's checkpoint loader node; "
|
| 38 |
+
"w4a4/int8 load with the stock CheckpointLoaderSimple")
|
| 39 |
+
ap.add_argument("--te-format", choices=sorted(TE_FORMATS), default="w4a4",
|
| 40 |
+
help="text encoder side. No low-rank branch either way")
|
| 41 |
+
ap.add_argument("--rank", type=int, default=64, help="diffusion side, svdq only")
|
| 42 |
+
ap.add_argument("--rank-alloc", default="uniform")
|
| 43 |
+
ap.add_argument("--refine-iters", type=int, default=100)
|
| 44 |
+
ap.add_argument("--refine-tol", type=float, default=REFINE_TOL)
|
| 45 |
+
ap.add_argument("--groupsize", type=int, default=256)
|
| 46 |
+
ap.add_argument("--act-stats", default=None, help="diffusion side only")
|
| 47 |
+
ap.add_argument("--variant", choices=["turbo", "base", "unknown"], default="unknown")
|
| 48 |
+
ap.add_argument("--seed", type=int, default=DEFAULT_SEED)
|
| 49 |
+
ap.add_argument("--device", default="cuda")
|
| 50 |
+
ap.add_argument("--out", default=None)
|
| 51 |
+
ap.add_argument("--keep-dit-temp", action="store_true",
|
| 52 |
+
help="do not delete the intermediate quantized diffusion file")
|
| 53 |
+
ap.add_argument("--dry-run", action="store_true",
|
| 54 |
+
help="map every key and print the size arithmetic. Quantizes nothing, "
|
| 55 |
+
"allocates nothing, never touches the GPU")
|
| 56 |
+
args = ap.parse_args()
|
| 57 |
+
|
| 58 |
+
fmt, rank = resolve_format(args.format, args.rank, rank_was_set=True)
|
| 59 |
+
|
| 60 |
+
out = args.out
|
| 61 |
+
if out is None:
|
| 62 |
+
stem = "Krea2-{}".format(args.variant.capitalize()) if args.variant != "unknown" \
|
| 63 |
+
else "Krea2"
|
| 64 |
+
tag = "SVDQuant-W4A4-rank{}".format(rank) if rank else args.format.upper()
|
| 65 |
+
dit_dir = os.path.dirname(os.path.abspath(args.dit))
|
| 66 |
+
ckpt_dir = os.path.join(os.path.dirname(dit_dir), "checkpoints")
|
| 67 |
+
target_dir = ckpt_dir if os.path.isdir(ckpt_dir) else dit_dir
|
| 68 |
+
out = os.path.join(target_dir,
|
| 69 |
+
"{}-AllInOne-{}-TE{}.safetensors".format(
|
| 70 |
+
stem, tag, args.te_format.upper()))
|
| 71 |
+
|
| 72 |
+
print("diffusion : {}".format(os.path.basename(args.dit)))
|
| 73 |
+
print("encoder : {} -> {}".format(os.path.basename(args.text_encoder), args.te_format))
|
| 74 |
+
print("vae : {} (unquantized)".format(os.path.basename(args.vae)))
|
| 75 |
+
print("out : {}".format(out))
|
| 76 |
+
|
| 77 |
+
summary = build_all_in_one_checkpoint(
|
| 78 |
+
dit_path=args.dit,
|
| 79 |
+
text_encoder_path=args.text_encoder,
|
| 80 |
+
vae_path=args.vae,
|
| 81 |
+
out_path=out,
|
| 82 |
+
fmt_name=args.format,
|
| 83 |
+
te_format=args.te_format,
|
| 84 |
+
rank=rank,
|
| 85 |
+
variant=args.variant,
|
| 86 |
+
groupsize=args.groupsize,
|
| 87 |
+
device=args.device,
|
| 88 |
+
dry_run=args.dry_run,
|
| 89 |
+
)
|
| 90 |
+
print("\n" + summary)
|
| 91 |
+
if not args.dry_run:
|
| 92 |
+
if rank:
|
| 93 |
+
print("svdq checkpoint: load it with the Krea2 SVDQuant Checkpoint Loader node, "
|
| 94 |
+
"not CheckpointLoaderSimple -- the low-rank branch needs attaching.")
|
| 95 |
+
else:
|
| 96 |
+
print("branchless: loads with the stock CheckpointLoaderSimple.")
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
if __name__ == "__main__":
|
| 100 |
+
main()
|
tools/build_workflows.py
CHANGED
|
@@ -37,7 +37,12 @@ ROW = 200 # vertical gap between rows
|
|
| 37 |
TEAL = ("#233", "#355") # loaders
|
| 38 |
GREEN = ("#232", "#353") # prompts
|
| 39 |
PURPLE = ("#323", "#535") # sampling
|
| 40 |
-
YELLOW = ("#432", "#653") # notes / read me first
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
class Graph:
|
|
@@ -56,7 +61,7 @@ class Graph:
|
|
| 56 |
self._api = {} # node_id -> what the API dialect needs about that node
|
| 57 |
|
| 58 |
def add(self, class_type, col, row, inputs=None, outputs=None, widgets=None,
|
| 59 |
-
title=None, colour=None, size=None, api=True):
|
| 60 |
"""`widgets` is a list of (input_name, value).
|
| 61 |
|
| 62 |
The name is what the API dialect needs and the UI dialect ignores; `None` marks a
|
|
@@ -66,7 +71,17 @@ class Graph:
|
|
| 66 |
|
| 67 |
`api=False` drops the node from the API output: Notes and the optional Diagnostics
|
| 68 |
node are editor furniture with nothing to execute.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
"""
|
|
|
|
|
|
|
| 70 |
nid = self._next_node
|
| 71 |
self._next_node += 1
|
| 72 |
inputs = inputs or []
|
|
@@ -83,8 +98,10 @@ class Graph:
|
|
| 83 |
"size": size or [380, 100],
|
| 84 |
"flags": {},
|
| 85 |
"order": self._next_node - 2,
|
| 86 |
-
"mode":
|
| 87 |
-
"inputs": [{"name": n, "type": t, "link": None}
|
|
|
|
|
|
|
| 88 |
"outputs": [{"name": n, "type": t, "links": [], "slot_index": i}
|
| 89 |
for i, (n, t) in enumerate(outputs)],
|
| 90 |
"properties": {"Node name for S&R": class_type},
|
|
@@ -191,6 +208,23 @@ TURBO_NOTE = """KREA 2 TURBO - SVDQuant W4A4
|
|
| 191 |
5. LoRA: use the Krea2 SVDQuant LoRA Loader, not the stock one. The
|
| 192 |
stock loader cannot patch a quantized weight and skips the blocks."""
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
BASE_NOTE = """KREA 2 BASE (non-turbo) - SVDQuant W4A4
|
| 195 |
|
| 196 |
The loader node will be outlined in RED when you open this. That is
|
|
@@ -221,6 +255,68 @@ checkpoint, then reselect it in the loader.
|
|
| 221 |
Same slow-generation checklist too: read the loader's status
|
| 222 |
output first."""
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
PROMPT = ("A cluttered antique clockmaker's workshop seen through a cracked magnifying "
|
| 225 |
"glass, brass gears laid out in a spiral on the workbench, a ginger cat asleep "
|
| 226 |
"on a stack of leather-bound books, warm afternoon light with visible dust "
|
|
@@ -296,6 +392,66 @@ def build_turbo():
|
|
| 296 |
return g.serialize(groups), g.serialize_api()
|
| 297 |
|
| 298 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
def build_base():
|
| 300 |
g = Graph()
|
| 301 |
g.note(BASE_NOTE, 0, 0, size=(400, 560), title="READ ME FIRST")
|
|
@@ -356,6 +512,399 @@ def build_base():
|
|
| 356 |
return g.serialize(groups), g.serialize_api()
|
| 357 |
|
| 358 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
def _write(path, payload):
|
| 360 |
with open(path, "w", encoding="utf-8", newline="\n") as fh:
|
| 361 |
json.dump(payload, fh, indent=2, ensure_ascii=False)
|
|
@@ -363,9 +912,16 @@ def _write(path, payload):
|
|
| 363 |
|
| 364 |
|
| 365 |
def main():
|
|
|
|
| 366 |
os.makedirs(OUT_DIR, exist_ok=True)
|
| 367 |
for name, build in (("krea2_turbo_svdquant_w4a4_t2i", build_turbo),
|
| 368 |
-
("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
graph, api = build()
|
| 370 |
ui_path = os.path.join(OUT_DIR, name + ".json")
|
| 371 |
api_path = os.path.join(OUT_DIR, name + "_api.json")
|
|
|
|
| 37 |
TEAL = ("#233", "#355") # loaders
|
| 38 |
GREEN = ("#232", "#353") # prompts
|
| 39 |
PURPLE = ("#323", "#535") # sampling
|
| 40 |
+
YELLOW = ("#432", "#653") # notes / read me first / diagnostics
|
| 41 |
+
ORANGE = ("#653", "#874") # LoRA
|
| 42 |
+
|
| 43 |
+
# Mirrors svdquant_lora.ADAPTER_BYPASS. Not imported: this script must run without ComfyUI
|
| 44 |
+
# on sys.path, and the loader module imports comfy at module level. Asserted in main().
|
| 45 |
+
ADAPTER_BYPASS = "bypass (exact, slower)"
|
| 46 |
|
| 47 |
|
| 48 |
class Graph:
|
|
|
|
| 61 |
self._api = {} # node_id -> what the API dialect needs about that node
|
| 62 |
|
| 63 |
def add(self, class_type, col, row, inputs=None, outputs=None, widgets=None,
|
| 64 |
+
title=None, colour=None, size=None, api=True, mode=0, widget_inputs=()):
|
| 65 |
"""`widgets` is a list of (input_name, value).
|
| 66 |
|
| 67 |
The name is what the API dialect needs and the UI dialect ignores; `None` marks a
|
|
|
|
| 71 |
|
| 72 |
`api=False` drops the node from the API output: Notes and the optional Diagnostics
|
| 73 |
node are editor furniture with nothing to execute.
|
| 74 |
+
|
| 75 |
+
`mode=4` mutes the node in the editor. A muted node must also be `api=False` -- the
|
| 76 |
+
API dialect has no mute flag, so leaving it in would execute something the editor
|
| 77 |
+
shows as off.
|
| 78 |
+
|
| 79 |
+
`widget_inputs` names inputs that are widgets on the node class but are being wired
|
| 80 |
+
from another node instead. LiteGraph needs the `widget` key on the slot or the editor
|
| 81 |
+
draws a plain input the frontend then refuses to reconnect to its widget.
|
| 82 |
"""
|
| 83 |
+
if mode == 4 and api:
|
| 84 |
+
raise ValueError("a muted node must be api=False: {}".format(class_type))
|
| 85 |
nid = self._next_node
|
| 86 |
self._next_node += 1
|
| 87 |
inputs = inputs or []
|
|
|
|
| 98 |
"size": size or [380, 100],
|
| 99 |
"flags": {},
|
| 100 |
"order": self._next_node - 2,
|
| 101 |
+
"mode": mode,
|
| 102 |
+
"inputs": [dict({"name": n, "type": t, "link": None},
|
| 103 |
+
**({"widget": {"name": n}} if n in widget_inputs else {}))
|
| 104 |
+
for n, t in inputs],
|
| 105 |
"outputs": [{"name": n, "type": t, "links": [], "slot_index": i}
|
| 106 |
for i, (n, t) in enumerate(outputs)],
|
| 107 |
"properties": {"Node name for S&R": class_type},
|
|
|
|
| 208 |
5. LoRA: use the Krea2 SVDQuant LoRA Loader, not the stock one. The
|
| 209 |
stock loader cannot patch a quantized weight and skips the blocks."""
|
| 210 |
|
| 211 |
+
ALL_IN_ONE_TURBO_NOTE = """KREA 2 TURBO - ALL-IN-ONE CHECKPOINT
|
| 212 |
+
|
| 213 |
+
1. Checkpoint goes in ComfyUI/models/checkpoints/.
|
| 214 |
+
This file is fully self-contained: it combines the diffusion model,
|
| 215 |
+
the 4-bit quantized text encoder (Qwen3-VL 4B), and the VAE into a
|
| 216 |
+
single safetensors file (~12 GB instead of 15.5 GB across 3 files).
|
| 217 |
+
|
| 218 |
+
2. SVDQuant checkpoints (carrying low-rank factors) load with the
|
| 219 |
+
"Krea2 SVDQuant Checkpoint Loader" node. Branchless checkpoints
|
| 220 |
+
(--format w4a4 or int8) also load with stock CheckpointLoaderSimple.
|
| 221 |
+
|
| 222 |
+
3. cfg MUST be 1.0. Krea 2 Turbo is cfg-distilled, so the negative
|
| 223 |
+
prompt is zeroed out (ConditioningZeroOut) rather than encoded.
|
| 224 |
+
8 steps with euler / simple scheduler is what the model was distilled for.
|
| 225 |
+
|
| 226 |
+
4. 1024x1024 native resolution. Latent size MUST be multiples of 16."""
|
| 227 |
+
|
| 228 |
BASE_NOTE = """KREA 2 BASE (non-turbo) - SVDQuant W4A4
|
| 229 |
|
| 230 |
The loader node will be outlined in RED when you open this. That is
|
|
|
|
| 255 |
Same slow-generation checklist too: read the loader's status
|
| 256 |
output first."""
|
| 257 |
|
| 258 |
+
LORA_NOTE = """KREA 2 TURBO + LoRA - SVDQuant W4A4
|
| 259 |
+
|
| 260 |
+
Same graph as the plain Turbo one, with LoRA loaders inserted between
|
| 261 |
+
the checkpoint loader and the sampler.
|
| 262 |
+
|
| 263 |
+
1. USE THIS NODE, NOT THE STOCK ONE. On a quantized checkpoint the
|
| 264 |
+
stock LoraLoaderModelOnly has to dequantize the 4-bit weight, add
|
| 265 |
+
the LoRA, and requantize it -- which throws away the format and
|
| 266 |
+
quantizes the LoRA delta to 4 bits along with it. This node
|
| 267 |
+
attaches the LoRA as a parallel branch instead:
|
| 268 |
+
(W + BA)x == Wx + B(Ax), so the quantized weight is never touched.
|
| 269 |
+
|
| 270 |
+
2. CHECK THE CONSOLE. The node prints what it matched, e.g.
|
| 271 |
+
224 quantized layers, 32 normal layers
|
| 272 |
+
If it says 0 quantized layers, the LoRA does not target this model
|
| 273 |
+
and nothing will change. That line is the first thing to look at
|
| 274 |
+
when a LoRA "does nothing".
|
| 275 |
+
|
| 276 |
+
3. STACKING. Chain more of these nodes, one per LoRA. The whole stack
|
| 277 |
+
is refolded each time, so strengths stay exact no matter how many
|
| 278 |
+
you chain, and the cost stays one pair of GEMMs. The second loader
|
| 279 |
+
here is bypassed (ctrl-B) -- unmute it to use it.
|
| 280 |
+
|
| 281 |
+
4. RANK MATTERS HERE. Without a LoRA, branch ranks 64/128/256 measure
|
| 282 |
+
the same. With one, rank 256 wins clearly and rank 64 loses most of
|
| 283 |
+
its advantage. If you use LoRAs, download rank 256.
|
| 284 |
+
|
| 285 |
+
5. adapters INPUT. Only affects LoKr / LoHa / OFT, which cannot fold
|
| 286 |
+
into the branch. "bypass" is exact and costs ~1.8s per model call
|
| 287 |
+
at 1440x1920; "bake" is free per-step but requantizes the delta.
|
| 288 |
+
A plain up/down LoRA ignores this setting entirely."""
|
| 289 |
+
|
| 290 |
+
DIAG_NOTE = """KREA 2 SVDQUANT - DIAGNOSTICS
|
| 291 |
+
|
| 292 |
+
Run this before reporting a bug, and before downloading 8 GB.
|
| 293 |
+
Nothing here generates an image.
|
| 294 |
+
|
| 295 |
+
ENV CHECK (left, no model needed)
|
| 296 |
+
Answers the single most common report: "the quantized checkpoint
|
| 297 |
+
is slower than fp8". Almost always the cause is that ComfyUI
|
| 298 |
+
disabled comfy_kitchen's CUDA backend because torch was built
|
| 299 |
+
against CUDA < 13, and the fallback dequantizes int4 in Python --
|
| 300 |
+
slower than bf16, not faster. Needs no model, so run it first.
|
| 301 |
+
|
| 302 |
+
DIAGNOSTICS (right, needs a checkpoint) - the mode widget:
|
| 303 |
+
dispatch which kernel actually runs. START HERE.
|
| 304 |
+
env versions + memory accounting, including where the
|
| 305 |
+
low-rank factors currently live. Under lowvram they
|
| 306 |
+
should read cpu between steps, not cuda.
|
| 307 |
+
bench quantized vs bf16 timing per layer.
|
| 308 |
+
profile torch.profiler table.
|
| 309 |
+
compile graph breaks a TorchCompileModel run would pay.
|
| 310 |
+
|
| 311 |
+
tokens = sequence length. 4096 is 1024x1024. Kernel selection is
|
| 312 |
+
shape-dependent, so match your real resolution.
|
| 313 |
+
|
| 314 |
+
WHAT TO PASTE INTO AN ISSUE
|
| 315 |
+
Your GPU, the checkpoint filename, and the report output of both
|
| 316 |
+
nodes. Both are OUTPUT_NODEs, so the text shows in the node itself
|
| 317 |
+
and in the console. Neither is cached -- they re-measure the live
|
| 318 |
+
process every run, which is the point."""
|
| 319 |
+
|
| 320 |
PROMPT = ("A cluttered antique clockmaker's workshop seen through a cracked magnifying "
|
| 321 |
"glass, brass gears laid out in a spiral on the workbench, a ginger cat asleep "
|
| 322 |
"on a stack of leather-bound books, warm afternoon light with visible dust "
|
|
|
|
| 392 |
return g.serialize(groups), g.serialize_api()
|
| 393 |
|
| 394 |
|
| 395 |
+
def build_all_in_one_turbo():
|
| 396 |
+
g = Graph()
|
| 397 |
+
g.note(ALL_IN_ONE_TURBO_NOTE, 0, 0, size=(400, 620), title="READ ME FIRST")
|
| 398 |
+
|
| 399 |
+
loader = g.add("Krea2SVDQuantCheckpointLoader", 1, 0,
|
| 400 |
+
outputs=[("MODEL", "MODEL"), ("CLIP", "CLIP"), ("VAE", "VAE"), ("STRING", "STRING")],
|
| 401 |
+
widgets=[("ckpt_name", "Krea2-Turbo-AllInOne-SVDQuant-W4A4-rank256-actaware-TEW4A4.safetensors")],
|
| 402 |
+
title="Krea2 SVDQuant Checkpoint Loader", colour=TEAL, size=[400, 140])
|
| 403 |
+
|
| 404 |
+
pos = g.add("CLIPTextEncode", 2, 0, inputs=[("clip", "CLIP")],
|
| 405 |
+
outputs=[("CONDITIONING", "CONDITIONING")], widgets=[("text", PROMPT)],
|
| 406 |
+
title="Prompt", colour=GREEN, size=[400, 220])
|
| 407 |
+
neg = g.add("ConditioningZeroOut", 2, 1.4, inputs=[("conditioning", "CONDITIONING")],
|
| 408 |
+
outputs=[("CONDITIONING", "CONDITIONING")],
|
| 409 |
+
title="Negative (zeroed - required at cfg 1.0)", colour=GREEN,
|
| 410 |
+
size=[400, 60])
|
| 411 |
+
latent = g.add("EmptySD3LatentImage", 2, 2.2, outputs=[("LATENT", "LATENT")],
|
| 412 |
+
widgets=[("width", 1024), ("height", 1024), ("batch_size", 1)],
|
| 413 |
+
title="Latent 1024x1024", colour=GREEN,
|
| 414 |
+
size=[400, 120])
|
| 415 |
+
|
| 416 |
+
sampler = g.add("KSampler", 3, 0,
|
| 417 |
+
inputs=[("model", "MODEL"), ("positive", "CONDITIONING"),
|
| 418 |
+
("negative", "CONDITIONING"), ("latent_image", "LATENT")],
|
| 419 |
+
outputs=[("LATENT", "LATENT")],
|
| 420 |
+
widgets=[("seed", 987654321), (None, "randomize"), ("steps", 8), ("cfg", 1.0),
|
| 421 |
+
("sampler_name", "euler"), ("scheduler", "simple"),
|
| 422 |
+
("denoise", 1.0)],
|
| 423 |
+
title="KSampler - 8 steps, cfg 1.0", colour=PURPLE, size=[400, 280])
|
| 424 |
+
decode = g.add("VAEDecode", 4, 0, inputs=[("samples", "LATENT"), ("vae", "VAE")],
|
| 425 |
+
outputs=[("IMAGE", "IMAGE")], title="VAE Decode", colour=PURPLE,
|
| 426 |
+
size=[300, 60])
|
| 427 |
+
save = g.add("SaveImage", 4, 0.7, inputs=[("images", "IMAGE")],
|
| 428 |
+
widgets=[("filename_prefix", "krea2_turbo_aio_svdq")], title="Save", colour=PURPLE, size=[400, 300])
|
| 429 |
+
|
| 430 |
+
diag = g.add("Krea2SVDQuantDiagnostics", 3, 2.4, inputs=[("model", "MODEL")],
|
| 431 |
+
outputs=[("MODEL", "MODEL"), ("STRING", "STRING")],
|
| 432 |
+
widgets=[("mode", "dispatch"), ("tokens", 4096)],
|
| 433 |
+
title="Diagnostics (optional - run if slow)", colour=YELLOW,
|
| 434 |
+
size=[400, 130], api=False)
|
| 435 |
+
|
| 436 |
+
g.link(loader, "MODEL", sampler, "model")
|
| 437 |
+
g.link(loader, "CLIP", pos, "clip")
|
| 438 |
+
g.link(pos, "CONDITIONING", neg, "conditioning")
|
| 439 |
+
g.link(pos, "CONDITIONING", sampler, "positive")
|
| 440 |
+
g.link(neg, "CONDITIONING", sampler, "negative")
|
| 441 |
+
g.link(latent, "LATENT", sampler, "latent_image")
|
| 442 |
+
g.link(sampler, "LATENT", decode, "samples")
|
| 443 |
+
g.link(loader, "VAE", decode, "vae")
|
| 444 |
+
g.link(decode, "IMAGE", save, "images")
|
| 445 |
+
g.link(loader, "MODEL", diag, "model")
|
| 446 |
+
|
| 447 |
+
groups = [
|
| 448 |
+
g.group("Load All-in-One", (500, 20, 420, 280)),
|
| 449 |
+
g.group("Prompt", (940, 20, 420, 620)),
|
| 450 |
+
g.group("Sample", (1360, 20, 420, 640), colour="#8a4"),
|
| 451 |
+
]
|
| 452 |
+
return g.serialize(groups), g.serialize_api()
|
| 453 |
+
|
| 454 |
+
|
| 455 |
def build_base():
|
| 456 |
g = Graph()
|
| 457 |
g.note(BASE_NOTE, 0, 0, size=(400, 560), title="READ ME FIRST")
|
|
|
|
| 512 |
return g.serialize(groups), g.serialize_api()
|
| 513 |
|
| 514 |
|
| 515 |
+
def build_lora():
|
| 516 |
+
"""The Turbo graph with two LoRA loaders spliced in, the second one muted.
|
| 517 |
+
|
| 518 |
+
Two rather than one because stacking is the question people actually ask, and a muted
|
| 519 |
+
second node shows the answer (chain them) without changing what the graph does on first
|
| 520 |
+
run.
|
| 521 |
+
"""
|
| 522 |
+
g = Graph()
|
| 523 |
+
g.note(LORA_NOTE, 0, 0, size=(400, 720), title="READ ME FIRST")
|
| 524 |
+
|
| 525 |
+
loader = g.add("Krea2SVDQuantW4A4Loader", 1, 0,
|
| 526 |
+
outputs=[("MODEL", "MODEL"), ("STRING", "STRING")],
|
| 527 |
+
widgets=[("model_name", "Krea2-Turbo-SVDQuant-W4A4-rank256.safetensors")],
|
| 528 |
+
title="Krea2 SVDQuant W4A4 Loader (rank 256 for LoRAs)", colour=TEAL,
|
| 529 |
+
size=[400, 120])
|
| 530 |
+
clip = g.add("CLIPLoader", 1, 1, outputs=[("CLIP", "CLIP")],
|
| 531 |
+
widgets=[("clip_name", "qwen3vl_4b_fp8_scaled.safetensors"), ("type", "krea2"),
|
| 532 |
+
("device", "default")],
|
| 533 |
+
title="Text encoder (Qwen3-VL 4B)", colour=TEAL, size=[400, 120])
|
| 534 |
+
vae = g.add("VAELoader", 1, 2, outputs=[("VAE", "VAE")],
|
| 535 |
+
widgets=[("vae_name", "qwen_image_vae.safetensors")], title="VAE", colour=TEAL,
|
| 536 |
+
size=[400, 80])
|
| 537 |
+
|
| 538 |
+
lora1 = g.add("Krea2SVDQuantLoraLoader", 2, 0, inputs=[("model", "MODEL")],
|
| 539 |
+
outputs=[("model", "MODEL")],
|
| 540 |
+
widgets=[("lora_name", "your_krea2_lora.safetensors"), ("strength", 1.0),
|
| 541 |
+
("adapters", ADAPTER_BYPASS)],
|
| 542 |
+
title="Krea2 SVDQuant LoRA Loader", colour=ORANGE, size=[400, 140])
|
| 543 |
+
lora2 = g.add("Krea2SVDQuantLoraLoader", 2, 1, inputs=[("model", "MODEL")],
|
| 544 |
+
outputs=[("model", "MODEL")],
|
| 545 |
+
widgets=[("lora_name", "your_krea2_lora.safetensors"), ("strength", 0.6),
|
| 546 |
+
("adapters", ADAPTER_BYPASS)],
|
| 547 |
+
title="Second LoRA (muted - ctrl-B to enable)", colour=ORANGE,
|
| 548 |
+
size=[400, 140], mode=4, api=False)
|
| 549 |
+
|
| 550 |
+
pos = g.add("CLIPTextEncode", 3, 0, inputs=[("clip", "CLIP")],
|
| 551 |
+
outputs=[("CONDITIONING", "CONDITIONING")], widgets=[("text", PROMPT)],
|
| 552 |
+
title="Prompt (add your LoRA trigger words)", colour=GREEN, size=[400, 220])
|
| 553 |
+
neg = g.add("ConditioningZeroOut", 3, 1.4, inputs=[("conditioning", "CONDITIONING")],
|
| 554 |
+
outputs=[("CONDITIONING", "CONDITIONING")],
|
| 555 |
+
title="Negative (zeroed - required at cfg 1.0)", colour=GREEN, size=[400, 60])
|
| 556 |
+
latent = g.add("EmptySD3LatentImage", 3, 2.2, outputs=[("LATENT", "LATENT")],
|
| 557 |
+
widgets=[("width", 1024), ("height", 1024), ("batch_size", 1)],
|
| 558 |
+
title="Latent 1024x1024", colour=GREEN, size=[400, 120])
|
| 559 |
+
|
| 560 |
+
sampler = g.add("KSampler", 4, 0,
|
| 561 |
+
inputs=[("model", "MODEL"), ("positive", "CONDITIONING"),
|
| 562 |
+
("negative", "CONDITIONING"), ("latent_image", "LATENT")],
|
| 563 |
+
outputs=[("LATENT", "LATENT")],
|
| 564 |
+
widgets=[("seed", 987654321), (None, "randomize"), ("steps", 8), ("cfg", 1.0),
|
| 565 |
+
("sampler_name", "euler"), ("scheduler", "simple"), ("denoise", 1.0)],
|
| 566 |
+
title="KSampler - 8 steps, cfg 1.0", colour=PURPLE, size=[400, 280])
|
| 567 |
+
decode = g.add("VAEDecode", 5, 0, inputs=[("samples", "LATENT"), ("vae", "VAE")],
|
| 568 |
+
outputs=[("IMAGE", "IMAGE")], title="VAE Decode", colour=PURPLE, size=[300, 60])
|
| 569 |
+
save = g.add("SaveImage", 5, 0.7, inputs=[("images", "IMAGE")],
|
| 570 |
+
widgets=[("filename_prefix", "krea2_turbo_lora")], title="Save", colour=PURPLE,
|
| 571 |
+
size=[400, 300])
|
| 572 |
+
|
| 573 |
+
g.link(loader, "MODEL", lora1, "model")
|
| 574 |
+
g.link(lora1, "model", lora2, "model")
|
| 575 |
+
g.link(lora1, "model", sampler, "model")
|
| 576 |
+
g.link(clip, "CLIP", pos, "clip")
|
| 577 |
+
g.link(pos, "CONDITIONING", neg, "conditioning")
|
| 578 |
+
g.link(pos, "CONDITIONING", sampler, "positive")
|
| 579 |
+
g.link(neg, "CONDITIONING", sampler, "negative")
|
| 580 |
+
g.link(latent, "LATENT", sampler, "latent_image")
|
| 581 |
+
g.link(sampler, "LATENT", decode, "samples")
|
| 582 |
+
g.link(vae, "VAE", decode, "vae")
|
| 583 |
+
g.link(decode, "IMAGE", save, "images")
|
| 584 |
+
|
| 585 |
+
groups = [
|
| 586 |
+
g.group("Load", (500, 20, 420, 620)),
|
| 587 |
+
g.group("LoRA - chain one node per LoRA", (940, 20, 420, 420), colour="#a85"),
|
| 588 |
+
g.group("Prompt", (1360, 20, 420, 620)),
|
| 589 |
+
g.group("Sample", (1780, 20, 420, 640), colour="#8a4"),
|
| 590 |
+
]
|
| 591 |
+
return g.serialize(groups), g.serialize_api()
|
| 592 |
+
|
| 593 |
+
|
| 594 |
+
def build_diagnostics():
|
| 595 |
+
"""Env Check + Diagnostics. No sampler: nothing here generates an image."""
|
| 596 |
+
g = Graph()
|
| 597 |
+
g.note(DIAG_NOTE, 0, 0, size=(430, 700), title="READ ME FIRST")
|
| 598 |
+
|
| 599 |
+
env = g.add("Krea2SVDQuantEnvCheck", 1, 0, outputs=[("report", "STRING")],
|
| 600 |
+
title="1. Env Check - no model needed, run this first", colour=YELLOW,
|
| 601 |
+
size=[400, 80])
|
| 602 |
+
env_view = g.add("PreviewAny", 1, 0.6, inputs=[("source", "*")],
|
| 603 |
+
title="Env Check report", colour=YELLOW, size=[400, 300])
|
| 604 |
+
|
| 605 |
+
loader = g.add("Krea2SVDQuantW4A4Loader", 2, 0,
|
| 606 |
+
outputs=[("MODEL", "MODEL"), ("STRING", "STRING")],
|
| 607 |
+
widgets=[("model_name", "Krea2-Turbo-SVDQuant-W4A4-rank256-actaware.safetensors")],
|
| 608 |
+
title="2. Load the checkpoint you are diagnosing", colour=TEAL,
|
| 609 |
+
size=[400, 120])
|
| 610 |
+
loader_view = g.add("PreviewAny", 2, 0.7, inputs=[("source", "*")],
|
| 611 |
+
title="Loader status - names the kernel that will run", colour=TEAL,
|
| 612 |
+
size=[400, 200])
|
| 613 |
+
|
| 614 |
+
diag = g.add("Krea2SVDQuantDiagnostics", 3, 0, inputs=[("model", "MODEL")],
|
| 615 |
+
outputs=[("model", "MODEL"), ("report", "STRING")],
|
| 616 |
+
widgets=[("mode", "dispatch"), ("tokens", 4096)],
|
| 617 |
+
title="3. Diagnostics - start with mode=dispatch", colour=YELLOW,
|
| 618 |
+
size=[400, 130])
|
| 619 |
+
diag_view = g.add("PreviewAny", 3, 0.8, inputs=[("source", "*")],
|
| 620 |
+
title="Diagnostics report - paste this into an issue", colour=YELLOW,
|
| 621 |
+
size=[400, 400])
|
| 622 |
+
|
| 623 |
+
g.link(env, "report", env_view, "source")
|
| 624 |
+
g.link(loader, "STRING", loader_view, "source")
|
| 625 |
+
g.link(loader, "MODEL", diag, "model")
|
| 626 |
+
g.link(diag, "report", diag_view, "source")
|
| 627 |
+
|
| 628 |
+
groups = [
|
| 629 |
+
g.group("Step 1 - does the int4 kernel exist at all?", (500, 20, 420, 480),
|
| 630 |
+
colour="#a85"),
|
| 631 |
+
g.group("Step 2 - load it", (940, 20, 420, 480)),
|
| 632 |
+
g.group("Step 3 - what actually runs", (1360, 20, 420, 620), colour="#8a4"),
|
| 633 |
+
]
|
| 634 |
+
return g.serialize(groups), g.serialize_api()
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
def _check_adapter_constant():
|
| 638 |
+
"""The `adapters` widget value must match svdquant_lora.ADAPTER_BYPASS exactly.
|
| 639 |
+
|
| 640 |
+
ComfyUI matches combo widgets by string. A drifted copy here would write a workflow whose
|
| 641 |
+
dropdown silently falls back to the first option, so read it out of the source rather than
|
| 642 |
+
trusting that two literals stayed equal.
|
| 643 |
+
"""
|
| 644 |
+
src = os.path.join(os.path.dirname(HERE), "svdquant_lora.py")
|
| 645 |
+
with open(src, encoding="utf-8") as fh:
|
| 646 |
+
for line in fh:
|
| 647 |
+
if line.startswith("ADAPTER_BYPASS"):
|
| 648 |
+
actual = line.split("=", 1)[1].strip().strip('"')
|
| 649 |
+
if actual != ADAPTER_BYPASS:
|
| 650 |
+
raise SystemExit(
|
| 651 |
+
"ADAPTER_BYPASS drifted: svdquant_lora.py has {!r}, this script has "
|
| 652 |
+
"{!r}".format(actual, ADAPTER_BYPASS))
|
| 653 |
+
return
|
| 654 |
+
raise SystemExit("could not find ADAPTER_BYPASS in " + src)
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
QUANTIZE_NOTE = """MAKE YOUR OWN QUANTIZED CHECKPOINT - no terminal needed
|
| 658 |
+
|
| 659 |
+
This graph is the whole quantizer. Set the widgets, press Queue,
|
| 660 |
+
wait. It does exactly what `python quantize_krea2.py` does (same
|
| 661 |
+
function, not a copy), and writes the result next to the source in
|
| 662 |
+
ComfyUI/models/diffusion_models/.
|
| 663 |
+
|
| 664 |
+
BEFORE YOU START
|
| 665 |
+
- You need the BF16 Krea 2 checkpoint (~24 GB) in
|
| 666 |
+
ComfyUI/models/diffusion_models/. FP8 also works as a source; any
|
| 667 |
+
other already-quantized file does not.
|
| 668 |
+
- ~12 GB free on that drive. The output is ~8 GB.
|
| 669 |
+
- Run the Env Check node first (it needs nothing wired). If it says
|
| 670 |
+
the cuda backend is not live, the file you are about to build will
|
| 671 |
+
run SLOWER than fp8 - fix torch before spending the disk.
|
| 672 |
+
|
| 673 |
+
WHICH FORMAT
|
| 674 |
+
svdq 4-bit + a low-rank bf16 correction branch. Best quality of
|
| 675 |
+
the 4-bit options. Loads with the Krea2 SVDQuant W4A4 Loader.
|
| 676 |
+
w4a4 same without the branch. Smaller, ~9% faster per step.
|
| 677 |
+
int8 most faithful, still ~2x fp8 on Ampere. Stock UNETLoader.
|
| 678 |
+
fp8 storage only, no speedup here.
|
| 679 |
+
w4a4 / int8 / fp8 ignore rank, rank_alloc and refine_iters.
|
| 680 |
+
|
| 681 |
+
WHICH RANK (svdq only)
|
| 682 |
+
No LoRA planned -> 64. 128 and 256 measured the same.
|
| 683 |
+
LoRAs planned -> 256. It wins clearly there; 64 loses most of
|
| 684 |
+
its advantage once a LoRA is patched in.
|
| 685 |
+
Keep refine_iters at 100. At 0 the split is single-shot (~54s
|
| 686 |
+
instead of ~5.7min) and raising rank then buys nothing.
|
| 687 |
+
|
| 688 |
+
WHAT TO EXPECT WHILE IT RUNS
|
| 689 |
+
- The queue is BLOCKED. Nothing else generates. 54s to ~6 min.
|
| 690 |
+
- Every loaded model is unloaded first, so your next generation
|
| 691 |
+
pays a reload. This is deliberate: the dequantize step needs the
|
| 692 |
+
card to itself or it OOMs.
|
| 693 |
+
- overwrite is off on purpose. An existing file of the same name is
|
| 694 |
+
an error, not 8 GB written over last night's run.
|
| 695 |
+
|
| 696 |
+
The summary appears on the node when it finishes, and says which
|
| 697 |
+
loader to use and what sampler settings that variant wants.
|
| 698 |
+
|
| 699 |
+
FREE QUALITY: FILL IN act_stats (svdq only)
|
| 700 |
+
Download the calibration file for your variant from
|
| 701 |
+
huggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI/tree/main/calibration
|
| 702 |
+
into ComfyUI/output/, then type its name into act_stats:
|
| 703 |
+
|
| 704 |
+
Turbo -> krea2_act_stats_turbo.safetensors
|
| 705 |
+
base -> krea2_act_stats_base.safetensors
|
| 706 |
+
|
| 707 |
+
6.67 MB each. They fit the low-rank branch against activations
|
| 708 |
+
measured from a real sampling pass instead of assuming every input
|
| 709 |
+
channel matters equally. LPIPS to BF16 0.3378 -> 0.2825 at rank 64.
|
| 710 |
+
Same rank, same file size, same kernel, same speed. There is no
|
| 711 |
+
reason to skip this.
|
| 712 |
+
|
| 713 |
+
Take the file that matches the model you are quantizing. A mismatch
|
| 714 |
+
is caught before any GPU work only if the layer NAMES differ, and
|
| 715 |
+
Turbo and base share their names - so the check will not save you
|
| 716 |
+
from feeding Turbo's statistics to a base build. Read the filename.
|
| 717 |
+
|
| 718 |
+
If you are quantizing a finetune, a merge, or anything these two
|
| 719 |
+
files do not describe, capture your own:
|
| 720 |
+
workflows/krea2_quantize_calibrated.json does the capture and the
|
| 721 |
+
quantize in one Queue press."""
|
| 722 |
+
|
| 723 |
+
CALIB_NOTE = """CALIBRATED QUANTIZE - measure your own activations
|
| 724 |
+
|
| 725 |
+
YOU PROBABLY DO NOT NEED THIS GRAPH.
|
| 726 |
+
Calibration files for stock Krea 2 Turbo and base are published at
|
| 727 |
+
huggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI/tree/main/calibration
|
| 728 |
+
- 6.67 MB each. Download the one matching your variant, drop it in
|
| 729 |
+
ComfyUI/output/, name it in the act_stats box of the plain
|
| 730 |
+
krea2_quantize.json graph, and you get the identical quality lever
|
| 731 |
+
without running a sampling pass.
|
| 732 |
+
|
| 733 |
+
This graph is for the case those files do not cover: a finetune, a
|
| 734 |
+
merge, a model whose weights are not the released ones. Activation
|
| 735 |
+
statistics describe the WEIGHTS they were captured from. Same
|
| 736 |
+
architecture is not the same model - Turbo's file loads cleanly into
|
| 737 |
+
a base build and quietly describes the wrong thing.
|
| 738 |
+
|
| 739 |
+
WHAT IT BUYS
|
| 740 |
+
The low-rank branch is fitted against activations measured from a
|
| 741 |
+
real sampling pass instead of assuming every input channel matters
|
| 742 |
+
equally. Measured, no LoRA, rank 64: LPIPS to BF16 0.3378 -> 0.2825.
|
| 743 |
+
Same rank, same file size, same kernel, same speed.
|
| 744 |
+
|
| 745 |
+
WHAT RUNS, IN ORDER
|
| 746 |
+
1. The BF16 model is loaded with the stock UNETLoader and sampled
|
| 747 |
+
once. Capture Start hooks every layer that will be quantized and
|
| 748 |
+
records the RMS of its inputs; Capture Save writes them out.
|
| 749 |
+
2. Capture Save's act_stats_path feeds the Quantize node. That wire
|
| 750 |
+
is not cosmetic - it is what stops ComfyUI running the quantizer
|
| 751 |
+
first, which would silently produce an uncalibrated file.
|
| 752 |
+
|
| 753 |
+
So one Queue press does calibration AND quantization. Expect the
|
| 754 |
+
sampling pass plus 54s-6min of quantizing, with the queue blocked.
|
| 755 |
+
|
| 756 |
+
SET THIS BEFORE YOU RUN
|
| 757 |
+
- UNETLoader: the BF16 checkpoint. Statistics from an already
|
| 758 |
+
quantized model describe the wrong thing.
|
| 759 |
+
- Quantize node: the SAME file as source_model. It is a separate
|
| 760 |
+
dropdown, and no, it cannot be wired - the loader hands over a
|
| 761 |
+
MODEL object, not a filename.
|
| 762 |
+
- variant: turbo or base, so the output gets the right name.
|
| 763 |
+
- KSampler: 20 steps / cfg 3.5 suits base. For Turbo use 8 steps
|
| 764 |
+
and cfg 1.0, and swap the negative prompt for ConditioningZeroOut.
|
| 765 |
+
|
| 766 |
+
CALIBRATING ON SEVERAL PROMPTS (better, optional)
|
| 767 |
+
The defaults calibrate on one prompt, which is enough to beat no
|
| 768 |
+
calibration. To use more: set keep_capturing = true on Capture Save,
|
| 769 |
+
mute the Quantize node (select it, Ctrl+M), queue as many prompts as
|
| 770 |
+
you like with reset = false on Capture Start after the first, then
|
| 771 |
+
unmute Quantize and queue once more. Prompts should look like what
|
| 772 |
+
you actually generate.
|
| 773 |
+
|
| 774 |
+
Only svdq uses act_stats. The other formats have no branch to fit,
|
| 775 |
+
and wiring act_stats into them is an error rather than a no-op."""
|
| 776 |
+
|
| 777 |
+
|
| 778 |
+
def build_quantize():
|
| 779 |
+
g = Graph()
|
| 780 |
+
g.note(QUANTIZE_NOTE, 0, 0, size=(460, 1360), title="READ ME FIRST")
|
| 781 |
+
|
| 782 |
+
g.add("Krea2SVDQuantEnvCheck", 1, 0, outputs=[("report", "STRING")],
|
| 783 |
+
title="1. Env Check - run this first, it needs nothing", colour=YELLOW,
|
| 784 |
+
size=[420, 120])
|
| 785 |
+
g.add("Krea2SVDQuantQuantize", 1, 0.9,
|
| 786 |
+
outputs=[("summary", "STRING")],
|
| 787 |
+
widgets=[("source_model", "krea2_bf16.safetensors"), ("format", "svdq"),
|
| 788 |
+
("rank", 64), ("rank_alloc", "uniform"), ("refine_iters", 100),
|
| 789 |
+
("groupsize", 256), ("variant", "turbo"), ("output_name", ""),
|
| 790 |
+
("overwrite", False), ("act_stats", ""), ("seed", 0)],
|
| 791 |
+
title="2. Quantize - set source_model and act_stats, then Queue", colour=TEAL,
|
| 792 |
+
size=[420, 460])
|
| 793 |
+
|
| 794 |
+
groups = [g.group("Quantize", (500, 20, 460, 640), colour="#8a4")]
|
| 795 |
+
return g.serialize(groups), g.serialize_api()
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
def build_quantize_all_in_one():
|
| 799 |
+
g = Graph()
|
| 800 |
+
note = """QUANTIZE ALL-IN-ONE - Bake DiT + Text Encoder + VAE
|
| 801 |
+
|
| 802 |
+
This graph bakes three files into one unified checkpoint in models/checkpoints/:
|
| 803 |
+
1. source_dit (in models/diffusion_models/): BF16 or already quantized.
|
| 804 |
+
2. text_encoder (in models/text_encoders/): Qwen3-VL 4B BF16 (~8.88 GB -> ~3.2 GB W4A4).
|
| 805 |
+
3. vae (in models/vae/): Krea2 VAE (~0.51 GB unquantized).
|
| 806 |
+
|
| 807 |
+
Resulting file: ~12 GB in models/checkpoints/, loaded with Krea2 SVDQuant Checkpoint Loader."""
|
| 808 |
+
g.note(note, 0, 0, size=(460, 600), title="READ ME FIRST")
|
| 809 |
+
|
| 810 |
+
g.add("Krea2SVDQuantEnvCheck", 1, 0, outputs=[("report", "STRING")],
|
| 811 |
+
title="1. Env Check - verify CUDA kernel", colour=YELLOW,
|
| 812 |
+
size=[420, 120])
|
| 813 |
+
g.add("Krea2SVDQuantQuantizeAllInOne", 1, 0.9,
|
| 814 |
+
outputs=[("summary", "STRING")],
|
| 815 |
+
widgets=[("source_dit", "turbo.safetensors"),
|
| 816 |
+
("text_encoder", "qwen3vl_4b_bf16.safetensors"),
|
| 817 |
+
("vae", "Krea2-HD-vae.safetensors"),
|
| 818 |
+
("format", "svdq"),
|
| 819 |
+
("te_format", "w4a4"),
|
| 820 |
+
("rank", 64),
|
| 821 |
+
("refine_iters", 100),
|
| 822 |
+
("groupsize", 256),
|
| 823 |
+
("variant", "turbo"),
|
| 824 |
+
("output_name", ""),
|
| 825 |
+
("overwrite", False),
|
| 826 |
+
("act_stats", "krea2_act_stats_turbo.safetensors"),
|
| 827 |
+
("seed", 0)],
|
| 828 |
+
title="2. Quantize & Bake All-in-One Checkpoint", colour=TEAL,
|
| 829 |
+
size=[420, 520])
|
| 830 |
+
|
| 831 |
+
groups = [g.group("All-in-One Baker", (500, 20, 460, 700), colour="#8a4")]
|
| 832 |
+
return g.serialize(groups), g.serialize_api()
|
| 833 |
+
|
| 834 |
+
|
| 835 |
+
def build_quantize_calibrated():
|
| 836 |
+
g = Graph()
|
| 837 |
+
g.note(CALIB_NOTE, 0, 0, size=(460, 1420), title="READ ME FIRST")
|
| 838 |
+
|
| 839 |
+
unet = g.add("UNETLoader", 1, 0, outputs=[("MODEL", "MODEL")],
|
| 840 |
+
widgets=[("unet_name", "krea2_bf16.safetensors"),
|
| 841 |
+
("weight_dtype", "default")],
|
| 842 |
+
title="BF16 source (stock loader)", colour=TEAL, size=[400, 120])
|
| 843 |
+
clip = g.add("CLIPLoader", 1, 1, outputs=[("CLIP", "CLIP")],
|
| 844 |
+
widgets=[("clip_name", "qwen3vl_4b_fp8_scaled.safetensors"), ("type", "krea2"),
|
| 845 |
+
("device", "default")],
|
| 846 |
+
title="Text encoder (Qwen3-VL 4B)", colour=TEAL, size=[400, 120])
|
| 847 |
+
|
| 848 |
+
start = g.add("Krea2SVDQuantCaptureStart", 2, 0, inputs=[("model", "MODEL")],
|
| 849 |
+
outputs=[("model", "MODEL"), ("status", "STRING")],
|
| 850 |
+
widgets=[("reset", True)],
|
| 851 |
+
title="Capture Start - before the sampler", colour=GREEN, size=[400, 100])
|
| 852 |
+
pos = g.add("CLIPTextEncode", 2, 0.8, inputs=[("clip", "CLIP")],
|
| 853 |
+
outputs=[("CONDITIONING", "CONDITIONING")], widgets=[("text", PROMPT)],
|
| 854 |
+
title="Calibration prompt", colour=GREEN, size=[400, 200])
|
| 855 |
+
neg = g.add("CLIPTextEncode", 2, 2.0, inputs=[("clip", "CLIP")],
|
| 856 |
+
outputs=[("CONDITIONING", "CONDITIONING")], widgets=[("text", NEGATIVE)],
|
| 857 |
+
title="Negative (Turbo: replace with ConditioningZeroOut)", colour=GREEN,
|
| 858 |
+
size=[400, 140])
|
| 859 |
+
latent = g.add("EmptySD3LatentImage", 2, 3.0, outputs=[("LATENT", "LATENT")],
|
| 860 |
+
widgets=[("width", 1024), ("height", 1024), ("batch_size", 1)],
|
| 861 |
+
title="Latent 1024x1024", colour=GREEN, size=[400, 120])
|
| 862 |
+
|
| 863 |
+
sampler = g.add("KSampler", 3, 0,
|
| 864 |
+
inputs=[("model", "MODEL"), ("positive", "CONDITIONING"),
|
| 865 |
+
("negative", "CONDITIONING"), ("latent_image", "LATENT")],
|
| 866 |
+
outputs=[("LATENT", "LATENT")],
|
| 867 |
+
widgets=[("seed", 987654321), (None, "randomize"), ("steps", 20),
|
| 868 |
+
("cfg", 3.5), ("sampler_name", "euler"), ("scheduler", "simple"),
|
| 869 |
+
("denoise", 1.0)],
|
| 870 |
+
title="Calibration pass - base 20/3.5, Turbo 8/1.0", colour=PURPLE,
|
| 871 |
+
size=[400, 280])
|
| 872 |
+
|
| 873 |
+
save = g.add("Krea2SVDQuantCaptureSave", 4, 0, inputs=[("latent", "LATENT")],
|
| 874 |
+
outputs=[("latent", "LATENT"), ("status", "STRING"),
|
| 875 |
+
("act_stats_path", "STRING")],
|
| 876 |
+
widgets=[("filename", "krea2_act_stats.safetensors"),
|
| 877 |
+
("keep_capturing", False)],
|
| 878 |
+
title="Capture Save - after the sampler", colour=GREEN, size=[400, 120])
|
| 879 |
+
|
| 880 |
+
quant = g.add("Krea2SVDQuantQuantize", 5, 0,
|
| 881 |
+
inputs=[("act_stats", "STRING")],
|
| 882 |
+
outputs=[("summary", "STRING")],
|
| 883 |
+
widgets=[("source_model", "krea2_bf16.safetensors"), ("format", "svdq"),
|
| 884 |
+
("rank", 64), ("rank_alloc", "uniform"), ("refine_iters", 100),
|
| 885 |
+
("groupsize", 256), ("variant", "turbo"), ("output_name", ""),
|
| 886 |
+
("overwrite", False), ("act_stats", ""), ("seed", 0)],
|
| 887 |
+
title="Quantize - source_model must match the loader above", colour=TEAL,
|
| 888 |
+
size=[420, 440], widget_inputs=("act_stats",))
|
| 889 |
+
|
| 890 |
+
g.link(unet, "MODEL", start, "model")
|
| 891 |
+
g.link(start, "model", sampler, "model")
|
| 892 |
+
g.link(clip, "CLIP", pos, "clip")
|
| 893 |
+
g.link(clip, "CLIP", neg, "clip")
|
| 894 |
+
g.link(pos, "CONDITIONING", sampler, "positive")
|
| 895 |
+
g.link(neg, "CONDITIONING", sampler, "negative")
|
| 896 |
+
g.link(latent, "LATENT", sampler, "latent_image")
|
| 897 |
+
g.link(sampler, "LATENT", save, "latent")
|
| 898 |
+
g.link(save, "act_stats_path", quant, "act_stats")
|
| 899 |
+
|
| 900 |
+
groups = [
|
| 901 |
+
g.group("Load BF16", (500, 20, 420, 420)),
|
| 902 |
+
g.group("Calibration pass", (940, 20, 420, 800)),
|
| 903 |
+
g.group("Quantize", (2200, 20, 460, 520), colour="#8a4"),
|
| 904 |
+
]
|
| 905 |
+
return g.serialize(groups), g.serialize_api()
|
| 906 |
+
|
| 907 |
+
|
| 908 |
def _write(path, payload):
|
| 909 |
with open(path, "w", encoding="utf-8", newline="\n") as fh:
|
| 910 |
json.dump(payload, fh, indent=2, ensure_ascii=False)
|
|
|
|
| 912 |
|
| 913 |
|
| 914 |
def main():
|
| 915 |
+
_check_adapter_constant()
|
| 916 |
os.makedirs(OUT_DIR, exist_ok=True)
|
| 917 |
for name, build in (("krea2_turbo_svdquant_w4a4_t2i", build_turbo),
|
| 918 |
+
("krea2_turbo_all_in_one_t2i", build_all_in_one_turbo),
|
| 919 |
+
("krea2_base_svdquant_w4a4_t2i", build_base),
|
| 920 |
+
("krea2_turbo_svdquant_w4a4_lora", build_lora),
|
| 921 |
+
("krea2_svdquant_diagnostics", build_diagnostics),
|
| 922 |
+
("krea2_quantize", build_quantize),
|
| 923 |
+
("krea2_quantize_all_in_one", build_quantize_all_in_one),
|
| 924 |
+
("krea2_quantize_calibrated", build_quantize_calibrated)):
|
| 925 |
graph, api = build()
|
| 926 |
ui_path = os.path.join(OUT_DIR, name + ".json")
|
| 927 |
api_path = os.path.join(OUT_DIR, name + "_api.json")
|
workflows/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Eight graphs. Drag the plain `.json` into the ComfyUI canvas — each opens with a **READ ME
|
| 2 |
+
FIRST** note carrying the settings that matter. The `*_api.json` twins are for POSTing to
|
| 3 |
+
`/prompt` from a script; they carry no layout, so don't drag those in.
|
| 4 |
+
|
| 5 |
+
| file | what it's for |
|
| 6 |
+
|---|---|
|
| 7 |
+
| `krea2_svdquant_diagnostics.json` | **Start here if anything is wrong.** No sampler, generates no image |
|
| 8 |
+
| `krea2_turbo_svdquant_w4a4_t2i.json` | Krea 2 **Turbo**, 8 steps, cfg 1.0 (3 separate files) |
|
| 9 |
+
| `krea2_turbo_all_in_one_t2i.json` | Krea 2 **Turbo All-in-One**, 8 steps, cfg 1.0 (single ~12 GB checkpoint) |
|
| 10 |
+
| `krea2_turbo_svdquant_w4a4_lora.json` | Turbo **with LoRAs**, chained through this repo's LoRA node |
|
| 11 |
+
| `krea2_base_svdquant_w4a4_t2i.json` | Krea 2 **base** (non-turbo), 50 steps, cfg 3.5 |
|
| 12 |
+
| `krea2_quantize.json` | **Build your own quantized DiT checkpoint** from a BF16 one, no terminal |
|
| 13 |
+
| `krea2_quantize_all_in_one.json` | **Build your own All-in-One checkpoint** (DiT + 4-bit TE + VAE), no terminal |
|
| 14 |
+
| `krea2_quantize_calibrated.json` | The same, preceded by an activation-calibration pass |
|
| 15 |
+
|
| 16 |
+
Everything under `workflows/` is generated by [`tools/build_workflows.py`](../tools/build_workflows.py).
|
| 17 |
+
Edit that script, not the JSON.
|
| 18 |
+
|
| 19 |
+
## What every graph needs
|
| 20 |
+
|
| 21 |
+
For standard workflows, three files:
|
| 22 |
+
|
| 23 |
+
1. **A checkpoint** in `ComfyUI/models/diffusion_models/`, from
|
| 24 |
+
[Hugging Face](https://huggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI).
|
| 25 |
+
2. **Text encoder** `qwen3vl_4b_fp8_scaled.safetensors` in `ComfyUI/models/text_encoders/`,
|
| 26 |
+
loaded with `CLIPLoader` type `krea2`.
|
| 27 |
+
3. **VAE** `qwen_image_vae.safetensors` in `ComfyUI/models/vae/`.
|
| 28 |
+
|
| 29 |
+
For **All-in-One** checkpoints, only **1 file** in `ComfyUI/models/checkpoints/`:
|
| 30 |
+
- Combines the quantized DiT, 4-bit quantized text encoder (Qwen3-VL 4B), and VAE into one ~12 GB file.
|
| 31 |
+
|
| 32 |
+
**Which loader node** depends on which checkpoint you downloaded:
|
| 33 |
+
|
| 34 |
+
| checkpoint | loader node |
|
| 35 |
+
|---|---|
|
| 36 |
+
| `Krea2-Turbo-W4A4-noLowRank` | the **stock UNETLoader** |
|
| 37 |
+
| any `SVDQuant-W4A4-rank*` | **Krea2 SVDQuant W4A4 Loader** (this repo) |
|
| 38 |
+
| All-in-One `SVDQuant-*-AllInOne-*` | **Krea2 SVDQuant Checkpoint Loader** (this repo) |
|
| 39 |
+
| All-in-One branchless (`W4A4` / `INT8`) | **CheckpointLoaderSimple** (stock) or **Krea2 SVDQuant Checkpoint Loader** |
|
| 40 |
+
|
| 41 |
+
The `svdq` files carry extra `*.svdq_l1` / `*.svdq_l2` tensors that the stock loader does not
|
| 42 |
+
know about, which is why they need their own node.
|
| 43 |
+
|
| 44 |
+
A **red outline** on a loader means ComfyUI cannot find the filename in the dropdown — the
|
| 45 |
+
graph ships a suggested name, and yours may differ. Click the widget and pick your file.
|
| 46 |
+
|
| 47 |
+
## Diagnostics — run this before reporting anything
|
| 48 |
+
|
| 49 |
+
`krea2_svdquant_diagnostics.json`. Three steps, left to right, and the first one needs no
|
| 50 |
+
model at all so you can run it *before* downloading 8 GB.
|
| 51 |
+
|
| 52 |
+
**1. Env Check.** Answers the single most common report against this pack: *"the quantized
|
| 53 |
+
checkpoint is slower than fp8"*. Almost always the cause is that ComfyUI disabled
|
| 54 |
+
`comfy_kitchen`'s CUDA backend because torch was built against CUDA < 13 — and the fallback
|
| 55 |
+
dequantizes int4 in Python, so the checkpoint ends up slower than bf16, not faster. You want
|
| 56 |
+
to see:
|
| 57 |
+
|
| 58 |
+
```
|
| 59 |
+
cuda backend is live -- the int4 tensor-core kernel is available.
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
If you don't, install a **cu130 (CUDA 13) or newer** PyTorch build. Nothing else in this repo
|
| 63 |
+
will behave until that line is green.
|
| 64 |
+
|
| 65 |
+
**2. Load the checkpoint** you're diagnosing. Its `status` output names the kernel that will
|
| 66 |
+
actually run.
|
| 67 |
+
|
| 68 |
+
**3. Diagnostics**, `mode` widget:
|
| 69 |
+
|
| 70 |
+
| mode | what it tells you |
|
| 71 |
+
|---|---|
|
| 72 |
+
| `dispatch` | which kernel actually runs, per weight shape. **Start here** |
|
| 73 |
+
| `env` | versions and memory accounting, including where the low-rank factors currently live — under lowvram they should read `cpu` between steps, not `cuda` |
|
| 74 |
+
| `bench` | quantized vs bf16 timing per layer |
|
| 75 |
+
| `profile` | a `torch.profiler` table |
|
| 76 |
+
| `compile` | graph breaks a `TorchCompileModel` run would pay |
|
| 77 |
+
|
| 78 |
+
`tokens` is the sequence length to probe with — 4096 is 1024x1024. Kernel selection is
|
| 79 |
+
shape-dependent, so match your real resolution.
|
| 80 |
+
|
| 81 |
+
Neither node is cached: they re-measure the live process every run, which is the entire
|
| 82 |
+
point. **Paste both reports, your GPU, and the checkpoint filename into any bug report.**
|
| 83 |
+
|
| 84 |
+
## LoRAs
|
| 85 |
+
|
| 86 |
+
`krea2_turbo_svdquant_w4a4_lora.json`.
|
| 87 |
+
|
| 88 |
+
**Use Krea2 SVDQuant LoRA Loader, not the stock `LoraLoaderModelOnly`.** On a quantized
|
| 89 |
+
checkpoint the stock loader has to dequantize the 4-bit weight, add the LoRA, and requantize
|
| 90 |
+
it — which discards the format and quantizes the LoRA delta to 4 bits along with it. This
|
| 91 |
+
node attaches the LoRA as a parallel branch instead: `(W + BA)x == Wx + B(Ax)` is exact for a
|
| 92 |
+
linear layer, and the quantized weight is never touched.
|
| 93 |
+
|
| 94 |
+
**If a LoRA seems to do nothing, read the console.** The node prints what it matched:
|
| 95 |
+
|
| 96 |
+
```
|
| 97 |
+
224 quantized layers, 32 normal layers
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
`0 quantized layers` means the LoRA does not target this model, and nothing will change no
|
| 101 |
+
matter what strength you set. That line is the first thing to check.
|
| 102 |
+
|
| 103 |
+
**Stacking.** Chain one node per LoRA — the shipped graph has a second one muted, press
|
| 104 |
+
`ctrl-B` on it to enable. The whole stack is refolded on each node, so strengths stay exact
|
| 105 |
+
however many you chain, and the runtime cost stays one pair of GEMMs rather than one per LoRA.
|
| 106 |
+
|
| 107 |
+
**Download rank 256 if you use LoRAs.** Without a LoRA, branch ranks 64 / 128 / 256 are
|
| 108 |
+
statistically indistinguishable. With one, rank 256 wins clearly and rank 64 loses most of its
|
| 109 |
+
advantage over having no branch at all — measured over 16 prompts x 2 seeds x 4 adapters in
|
| 110 |
+
[BENCHMARKS.md](../BENCHMARKS.md#test-3--paired-lpips-fidelity-with-and-without-a-lora).
|
| 111 |
+
|
| 112 |
+
**The `adapters` input** only affects LoRAs that cannot fold into the branch — LoKr, LoHa,
|
| 113 |
+
OFT. A plain up/down LoRA ignores it.
|
| 114 |
+
|
| 115 |
+
| value | cost | what happens |
|
| 116 |
+
|---|---|---|
|
| 117 |
+
| `bypass` (default) | ~1.8 s per model call at 1440x1920 | exact; the 4-bit weight is never touched |
|
| 118 |
+
| `bake` | none per step | ComfyUI rewrites the weight once and the adapter delta is quantized to 4 bits with it |
|
| 119 |
+
|
| 120 |
+
## Building your own checkpoint
|
| 121 |
+
|
| 122 |
+
`krea2_quantize.json` is the whole quantizer as a graph: point `source_model` at a BF16 Krea 2
|
| 123 |
+
checkpoint, pick a format and a rank, press Queue. It calls the same `convert()` function
|
| 124 |
+
`quantize_krea2.py` does, so there is one quantizer and not two that drift.
|
| 125 |
+
|
| 126 |
+
It blocks the queue for the whole run (54 s to ~6 min), unloads any resident model to take the
|
| 127 |
+
GPU, and writes ~8 GB. `overwrite` is off by default, so an existing file of the same name is
|
| 128 |
+
an error rather than 8 GB written over your last run.
|
| 129 |
+
|
| 130 |
+
**Fill in `act_stats`.** It is the largest fidelity gain in this repo that costs nothing at
|
| 131 |
+
runtime — same rank, same file size, same kernel, same speed — and for the released weights you
|
| 132 |
+
do not have to measure anything. Download the file for your variant from
|
| 133 |
+
[`calibration/`](https://huggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI/tree/main/calibration)
|
| 134 |
+
(6.67 MB) into `ComfyUI/output/` and type its name:
|
| 135 |
+
|
| 136 |
+
| you are quantizing | act_stats file |
|
| 137 |
+
|---|---|
|
| 138 |
+
| stock Krea 2 **Turbo** | `krea2_act_stats_turbo.safetensors` |
|
| 139 |
+
| stock Krea 2 **base** | `krea2_act_stats_base.safetensors` |
|
| 140 |
+
| a finetune, a merge, anything else | capture your own — see below |
|
| 141 |
+
|
| 142 |
+
`krea2_act_stats_turbo_lora_bloomgirls.safetensors` is also published. Do **not** reach for it because you use
|
| 143 |
+
LoRAs: it is the capture from the `bloomgirls` arm of
|
| 144 |
+
[Test 5](../BENCHMARKS.md), kept so that experiment is reproducible. Calibrating with an
|
| 145 |
+
adapter loaded measured marginally *worse* than calibrating without one, and the recommendation
|
| 146 |
+
is the plain file whether or not you load LoRAs.
|
| 147 |
+
|
| 148 |
+
## Capturing your own activation statistics
|
| 149 |
+
|
| 150 |
+
Only needed for weights the published files do not describe — a finetune, a merge, a model
|
| 151 |
+
whose weights are not the released ones. Activation statistics describe the **weights** they
|
| 152 |
+
were captured from, and sharing an architecture is not enough: Turbo's file loads into a base
|
| 153 |
+
build without a word of complaint and then describes the wrong model. The coverage check that
|
| 154 |
+
runs before quantizing matches on layer *names*, which Turbo and base share, so it cannot catch
|
| 155 |
+
that for you.
|
| 156 |
+
|
| 157 |
+
`krea2_quantize_calibrated.json` does it in one Queue press:
|
| 158 |
+
|
| 159 |
+
```
|
| 160 |
+
UNETLoader (BF16) → Capture Start → KSampler → Capture Save → Quantize
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
* **Capture Start** goes *before* the sampler. It hooks all 224 linears that will be quantized
|
| 164 |
+
and records the RMS of their inputs. Feed it the **BF16** model — statistics taken from an
|
| 165 |
+
already-quantized model describe the thing being corrected.
|
| 166 |
+
* **Capture Save** goes *after* the sampler. It takes the sampler's `LATENT`, which is what
|
| 167 |
+
forces it to run after denoising rather than before it; if it is not downstream of the
|
| 168 |
+
sampler, ComfyUI is free to execute it first and you get an empty capture.
|
| 169 |
+
* Its **`act_stats_path`** output feeds the Quantize node. That wire is not cosmetic — it is
|
| 170 |
+
what stops ComfyUI quantizing before the calibration pass has run and handing back an
|
| 171 |
+
uncalibrated file with no warning.
|
| 172 |
+
* Set the Quantize node's `source_model` to the **same file** the UNETLoader has. It is a
|
| 173 |
+
separate dropdown and cannot be wired: the loader hands over a MODEL object, not a filename.
|
| 174 |
+
|
| 175 |
+
**Calibrating on several prompts** (better, and what the published files did — 8 prompts each):
|
| 176 |
+
|
| 177 |
+
1. Set `keep_capturing` = true on Capture Save.
|
| 178 |
+
2. Mute the Quantize node (select it, Ctrl+M).
|
| 179 |
+
3. Queue your first prompt with `reset` = true on Capture Start.
|
| 180 |
+
4. Queue more prompts with `reset` = **false**. Use prompts that look like what you actually
|
| 181 |
+
generate, and *not* the ones you plan to judge the checkpoint with.
|
| 182 |
+
5. Unmute Quantize, set `keep_capturing` = false, queue once more.
|
| 183 |
+
|
| 184 |
+
Watch the Capture Save status: a clean run ends with every layer on the same token count (ours
|
| 185 |
+
was 264200/264200 across 224 layers). A spread means the hooks stayed live through something
|
| 186 |
+
other than your calibration run — usually a graph that errored between Start and Save — and the
|
| 187 |
+
statistics now describe more than you meant. Re-run with `reset` = true.
|
| 188 |
+
|
| 189 |
+
**One LoRA at a time is not a thing to calibrate for.** The statistics are per model, not per
|
| 190 |
+
LoRA: capture from the BF16 weights you are about to quantize, with no adapter loaded, and use
|
| 191 |
+
the result for every LoRA you later run on top of it. That is not a shortcut — it measured
|
| 192 |
+
better than capturing with the adapter loaded.
|
| 193 |
+
|
| 194 |
+
## Turbo vs base
|
| 195 |
+
|
| 196 |
+
**Turbo** is cfg-distilled: `cfg` **must** be 1.0 and the negative prompt is zeroed
|
| 197 |
+
(`ConditioningZeroOut`) rather than encoded. Raising cfg degrades the image, it does not
|
| 198 |
+
sharpen it. 8 steps is what the checkpoint was distilled for.
|
| 199 |
+
|
| 200 |
+
**Base** is not distilled, so it uses real classifier-free guidance — cfg 3.5, a real negative
|
| 201 |
+
prompt through a second `CLIPTextEncode`, ~50 steps. There is no pre-built base checkpoint to
|
| 202 |
+
download; build one:
|
| 203 |
+
|
| 204 |
+
```bash
|
| 205 |
+
python quantize_krea2.py raw.safetensors --format svdq --rank 256 --variant base
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
That is roughly 12x the sampling work of the 8-step Turbo graph. Every benchmark number in
|
| 209 |
+
the README is Turbo at 8 steps — the ratios carry over, the absolute seconds do not.
|
workflows/krea2_quantize.json
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "00000000-0000-4000-8000-000000000000",
|
| 3 |
+
"revision": 0,
|
| 4 |
+
"last_node_id": 3,
|
| 5 |
+
"last_link_id": 0,
|
| 6 |
+
"nodes": [
|
| 7 |
+
{
|
| 8 |
+
"id": 1,
|
| 9 |
+
"type": "Note",
|
| 10 |
+
"pos": [
|
| 11 |
+
60,
|
| 12 |
+
60
|
| 13 |
+
],
|
| 14 |
+
"size": [
|
| 15 |
+
460,
|
| 16 |
+
1360
|
| 17 |
+
],
|
| 18 |
+
"flags": {},
|
| 19 |
+
"order": 0,
|
| 20 |
+
"mode": 0,
|
| 21 |
+
"inputs": [],
|
| 22 |
+
"outputs": [],
|
| 23 |
+
"properties": {
|
| 24 |
+
"Node name for S&R": "Note"
|
| 25 |
+
},
|
| 26 |
+
"widgets_values": [
|
| 27 |
+
"MAKE YOUR OWN QUANTIZED CHECKPOINT - no terminal needed\n\nThis graph is the whole quantizer. Set the widgets, press Queue,\nwait. It does exactly what `python quantize_krea2.py` does (same\nfunction, not a copy), and writes the result next to the source in\nComfyUI/models/diffusion_models/.\n\nBEFORE YOU START\n- You need the BF16 Krea 2 checkpoint (~24 GB) in\n ComfyUI/models/diffusion_models/. FP8 also works as a source; any\n other already-quantized file does not.\n- ~12 GB free on that drive. The output is ~8 GB.\n- Run the Env Check node first (it needs nothing wired). If it says\n the cuda backend is not live, the file you are about to build will\n run SLOWER than fp8 - fix torch before spending the disk.\n\nWHICH FORMAT\n svdq 4-bit + a low-rank bf16 correction branch. Best quality of\n the 4-bit options. Loads with the Krea2 SVDQuant W4A4 Loader.\n w4a4 same without the branch. Smaller, ~9% faster per step.\n int8 most faithful, still ~2x fp8 on Ampere. Stock UNETLoader.\n fp8 storage only, no speedup here.\n w4a4 / int8 / fp8 ignore rank, rank_alloc and refine_iters.\n\nWHICH RANK (svdq only)\n No LoRA planned -> 64. 128 and 256 measured the same.\n LoRAs planned -> 256. It wins clearly there; 64 loses most of\n its advantage once a LoRA is patched in.\n Keep refine_iters at 100. At 0 the split is single-shot (~54s\n instead of ~5.7min) and raising rank then buys nothing.\n\nWHAT TO EXPECT WHILE IT RUNS\n- The queue is BLOCKED. Nothing else generates. 54s to ~6 min.\n- Every loaded model is unloaded first, so your next generation\n pays a reload. This is deliberate: the dequantize step needs the\n card to itself or it OOMs.\n- overwrite is off on purpose. An existing file of the same name is\n an error, not 8 GB written over last night's run.\n\nThe summary appears on the node when it finishes, and says which\nloader to use and what sampler settings that variant wants.\n\nFREE QUALITY: FILL IN act_stats (svdq only)\nDownload the calibration file for your variant from\nhuggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI/tree/main/calibration\ninto ComfyUI/output/, then type its name into act_stats:\n\n Turbo -> krea2_act_stats_turbo.safetensors\n base -> krea2_act_stats_base.safetensors\n\n6.67 MB each. They fit the low-rank branch against activations\nmeasured from a real sampling pass instead of assuming every input\nchannel matters equally. LPIPS to BF16 0.3378 -> 0.2825 at rank 64.\nSame rank, same file size, same kernel, same speed. There is no\nreason to skip this.\n\nTake the file that matches the model you are quantizing. A mismatch\nis caught before any GPU work only if the layer NAMES differ, and\nTurbo and base share their names - so the check will not save you\nfrom feeding Turbo's statistics to a base build. Read the filename.\n\nIf you are quantizing a finetune, a merge, or anything these two\nfiles do not describe, capture your own:\nworkflows/krea2_quantize_calibrated.json does the capture and the\nquantize in one Queue press."
|
| 28 |
+
],
|
| 29 |
+
"title": "READ ME FIRST",
|
| 30 |
+
"color": "#432",
|
| 31 |
+
"bgcolor": "#653"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": 2,
|
| 35 |
+
"type": "Krea2SVDQuantEnvCheck",
|
| 36 |
+
"pos": [
|
| 37 |
+
480,
|
| 38 |
+
60
|
| 39 |
+
],
|
| 40 |
+
"size": [
|
| 41 |
+
420,
|
| 42 |
+
120
|
| 43 |
+
],
|
| 44 |
+
"flags": {},
|
| 45 |
+
"order": 1,
|
| 46 |
+
"mode": 0,
|
| 47 |
+
"inputs": [],
|
| 48 |
+
"outputs": [
|
| 49 |
+
{
|
| 50 |
+
"name": "report",
|
| 51 |
+
"type": "STRING",
|
| 52 |
+
"links": [],
|
| 53 |
+
"slot_index": 0
|
| 54 |
+
}
|
| 55 |
+
],
|
| 56 |
+
"properties": {
|
| 57 |
+
"Node name for S&R": "Krea2SVDQuantEnvCheck"
|
| 58 |
+
},
|
| 59 |
+
"widgets_values": [],
|
| 60 |
+
"title": "1. Env Check - run this first, it needs nothing",
|
| 61 |
+
"color": "#432",
|
| 62 |
+
"bgcolor": "#653"
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"id": 3,
|
| 66 |
+
"type": "Krea2SVDQuantQuantize",
|
| 67 |
+
"pos": [
|
| 68 |
+
480,
|
| 69 |
+
240.0
|
| 70 |
+
],
|
| 71 |
+
"size": [
|
| 72 |
+
420,
|
| 73 |
+
460
|
| 74 |
+
],
|
| 75 |
+
"flags": {},
|
| 76 |
+
"order": 2,
|
| 77 |
+
"mode": 0,
|
| 78 |
+
"inputs": [],
|
| 79 |
+
"outputs": [
|
| 80 |
+
{
|
| 81 |
+
"name": "summary",
|
| 82 |
+
"type": "STRING",
|
| 83 |
+
"links": [],
|
| 84 |
+
"slot_index": 0
|
| 85 |
+
}
|
| 86 |
+
],
|
| 87 |
+
"properties": {
|
| 88 |
+
"Node name for S&R": "Krea2SVDQuantQuantize"
|
| 89 |
+
},
|
| 90 |
+
"widgets_values": [
|
| 91 |
+
"krea2_bf16.safetensors",
|
| 92 |
+
"svdq",
|
| 93 |
+
64,
|
| 94 |
+
"uniform",
|
| 95 |
+
100,
|
| 96 |
+
256,
|
| 97 |
+
"turbo",
|
| 98 |
+
"",
|
| 99 |
+
false,
|
| 100 |
+
"",
|
| 101 |
+
0
|
| 102 |
+
],
|
| 103 |
+
"title": "2. Quantize - set source_model and act_stats, then Queue",
|
| 104 |
+
"color": "#233",
|
| 105 |
+
"bgcolor": "#355"
|
| 106 |
+
}
|
| 107 |
+
],
|
| 108 |
+
"links": [],
|
| 109 |
+
"groups": [
|
| 110 |
+
{
|
| 111 |
+
"title": "Quantize",
|
| 112 |
+
"bounding": [
|
| 113 |
+
500,
|
| 114 |
+
20,
|
| 115 |
+
460,
|
| 116 |
+
640
|
| 117 |
+
],
|
| 118 |
+
"color": "#8a4",
|
| 119 |
+
"font_size": 24,
|
| 120 |
+
"flags": {}
|
| 121 |
+
}
|
| 122 |
+
],
|
| 123 |
+
"config": {},
|
| 124 |
+
"extra": {},
|
| 125 |
+
"version": 0.4
|
| 126 |
+
}
|
workflows/krea2_quantize_all_in_one.json
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "00000000-0000-4000-8000-000000000000",
|
| 3 |
+
"revision": 0,
|
| 4 |
+
"last_node_id": 3,
|
| 5 |
+
"last_link_id": 0,
|
| 6 |
+
"nodes": [
|
| 7 |
+
{
|
| 8 |
+
"id": 1,
|
| 9 |
+
"type": "Note",
|
| 10 |
+
"pos": [
|
| 11 |
+
60,
|
| 12 |
+
60
|
| 13 |
+
],
|
| 14 |
+
"size": [
|
| 15 |
+
460,
|
| 16 |
+
600
|
| 17 |
+
],
|
| 18 |
+
"flags": {},
|
| 19 |
+
"order": 0,
|
| 20 |
+
"mode": 0,
|
| 21 |
+
"inputs": [],
|
| 22 |
+
"outputs": [],
|
| 23 |
+
"properties": {
|
| 24 |
+
"Node name for S&R": "Note"
|
| 25 |
+
},
|
| 26 |
+
"widgets_values": [
|
| 27 |
+
"QUANTIZE ALL-IN-ONE - Bake DiT + Text Encoder + VAE\n\nThis graph bakes three files into one unified checkpoint in models/checkpoints/:\n1. source_dit (in models/diffusion_models/): BF16 or already quantized.\n2. text_encoder (in models/text_encoders/): Qwen3-VL 4B BF16 (~8.88 GB -> ~3.2 GB W4A4).\n3. vae (in models/vae/): Krea2 VAE (~0.51 GB unquantized).\n\nResulting file: ~12 GB in models/checkpoints/, loaded with Krea2 SVDQuant Checkpoint Loader."
|
| 28 |
+
],
|
| 29 |
+
"title": "READ ME FIRST",
|
| 30 |
+
"color": "#432",
|
| 31 |
+
"bgcolor": "#653"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": 2,
|
| 35 |
+
"type": "Krea2SVDQuantEnvCheck",
|
| 36 |
+
"pos": [
|
| 37 |
+
480,
|
| 38 |
+
60
|
| 39 |
+
],
|
| 40 |
+
"size": [
|
| 41 |
+
420,
|
| 42 |
+
120
|
| 43 |
+
],
|
| 44 |
+
"flags": {},
|
| 45 |
+
"order": 1,
|
| 46 |
+
"mode": 0,
|
| 47 |
+
"inputs": [],
|
| 48 |
+
"outputs": [
|
| 49 |
+
{
|
| 50 |
+
"name": "report",
|
| 51 |
+
"type": "STRING",
|
| 52 |
+
"links": [],
|
| 53 |
+
"slot_index": 0
|
| 54 |
+
}
|
| 55 |
+
],
|
| 56 |
+
"properties": {
|
| 57 |
+
"Node name for S&R": "Krea2SVDQuantEnvCheck"
|
| 58 |
+
},
|
| 59 |
+
"widgets_values": [],
|
| 60 |
+
"title": "1. Env Check - verify CUDA kernel",
|
| 61 |
+
"color": "#432",
|
| 62 |
+
"bgcolor": "#653"
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"id": 3,
|
| 66 |
+
"type": "Krea2SVDQuantQuantizeAllInOne",
|
| 67 |
+
"pos": [
|
| 68 |
+
480,
|
| 69 |
+
240.0
|
| 70 |
+
],
|
| 71 |
+
"size": [
|
| 72 |
+
420,
|
| 73 |
+
520
|
| 74 |
+
],
|
| 75 |
+
"flags": {},
|
| 76 |
+
"order": 2,
|
| 77 |
+
"mode": 0,
|
| 78 |
+
"inputs": [],
|
| 79 |
+
"outputs": [
|
| 80 |
+
{
|
| 81 |
+
"name": "summary",
|
| 82 |
+
"type": "STRING",
|
| 83 |
+
"links": [],
|
| 84 |
+
"slot_index": 0
|
| 85 |
+
}
|
| 86 |
+
],
|
| 87 |
+
"properties": {
|
| 88 |
+
"Node name for S&R": "Krea2SVDQuantQuantizeAllInOne"
|
| 89 |
+
},
|
| 90 |
+
"widgets_values": [
|
| 91 |
+
"turbo.safetensors",
|
| 92 |
+
"qwen3vl_4b_bf16.safetensors",
|
| 93 |
+
"Krea2-HD-vae.safetensors",
|
| 94 |
+
"svdq",
|
| 95 |
+
"w4a4",
|
| 96 |
+
64,
|
| 97 |
+
100,
|
| 98 |
+
256,
|
| 99 |
+
"turbo",
|
| 100 |
+
"",
|
| 101 |
+
false,
|
| 102 |
+
"krea2_act_stats_turbo.safetensors",
|
| 103 |
+
0
|
| 104 |
+
],
|
| 105 |
+
"title": "2. Quantize & Bake All-in-One Checkpoint",
|
| 106 |
+
"color": "#233",
|
| 107 |
+
"bgcolor": "#355"
|
| 108 |
+
}
|
| 109 |
+
],
|
| 110 |
+
"links": [],
|
| 111 |
+
"groups": [
|
| 112 |
+
{
|
| 113 |
+
"title": "All-in-One Baker",
|
| 114 |
+
"bounding": [
|
| 115 |
+
500,
|
| 116 |
+
20,
|
| 117 |
+
460,
|
| 118 |
+
700
|
| 119 |
+
],
|
| 120 |
+
"color": "#8a4",
|
| 121 |
+
"font_size": 24,
|
| 122 |
+
"flags": {}
|
| 123 |
+
}
|
| 124 |
+
],
|
| 125 |
+
"config": {},
|
| 126 |
+
"extra": {},
|
| 127 |
+
"version": 0.4
|
| 128 |
+
}
|
workflows/krea2_quantize_all_in_one_api.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"1": {
|
| 3 |
+
"class_type": "Krea2SVDQuantEnvCheck",
|
| 4 |
+
"inputs": {},
|
| 5 |
+
"_meta": {
|
| 6 |
+
"title": "1. Env Check - verify CUDA kernel"
|
| 7 |
+
}
|
| 8 |
+
},
|
| 9 |
+
"2": {
|
| 10 |
+
"class_type": "Krea2SVDQuantQuantizeAllInOne",
|
| 11 |
+
"inputs": {
|
| 12 |
+
"source_dit": "turbo.safetensors",
|
| 13 |
+
"text_encoder": "qwen3vl_4b_bf16.safetensors",
|
| 14 |
+
"vae": "Krea2-HD-vae.safetensors",
|
| 15 |
+
"format": "svdq",
|
| 16 |
+
"te_format": "w4a4",
|
| 17 |
+
"rank": 64,
|
| 18 |
+
"refine_iters": 100,
|
| 19 |
+
"groupsize": 256,
|
| 20 |
+
"variant": "turbo",
|
| 21 |
+
"output_name": "",
|
| 22 |
+
"overwrite": false,
|
| 23 |
+
"act_stats": "krea2_act_stats_turbo.safetensors",
|
| 24 |
+
"seed": 0
|
| 25 |
+
},
|
| 26 |
+
"_meta": {
|
| 27 |
+
"title": "2. Quantize & Bake All-in-One Checkpoint"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
workflows/krea2_quantize_api.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"1": {
|
| 3 |
+
"class_type": "Krea2SVDQuantEnvCheck",
|
| 4 |
+
"inputs": {},
|
| 5 |
+
"_meta": {
|
| 6 |
+
"title": "1. Env Check - run this first, it needs nothing"
|
| 7 |
+
}
|
| 8 |
+
},
|
| 9 |
+
"2": {
|
| 10 |
+
"class_type": "Krea2SVDQuantQuantize",
|
| 11 |
+
"inputs": {
|
| 12 |
+
"source_model": "krea2_bf16.safetensors",
|
| 13 |
+
"format": "svdq",
|
| 14 |
+
"rank": 64,
|
| 15 |
+
"rank_alloc": "uniform",
|
| 16 |
+
"refine_iters": 100,
|
| 17 |
+
"groupsize": 256,
|
| 18 |
+
"variant": "turbo",
|
| 19 |
+
"output_name": "",
|
| 20 |
+
"overwrite": false,
|
| 21 |
+
"act_stats": "",
|
| 22 |
+
"seed": 0
|
| 23 |
+
},
|
| 24 |
+
"_meta": {
|
| 25 |
+
"title": "2. Quantize - set source_model and act_stats, then Queue"
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
}
|
workflows/krea2_quantize_calibrated.json
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "00000000-0000-4000-8000-000000000000",
|
| 3 |
+
"revision": 0,
|
| 4 |
+
"last_node_id": 10,
|
| 5 |
+
"last_link_id": 9,
|
| 6 |
+
"nodes": [
|
| 7 |
+
{
|
| 8 |
+
"id": 1,
|
| 9 |
+
"type": "Note",
|
| 10 |
+
"pos": [
|
| 11 |
+
60,
|
| 12 |
+
60
|
| 13 |
+
],
|
| 14 |
+
"size": [
|
| 15 |
+
460,
|
| 16 |
+
1420
|
| 17 |
+
],
|
| 18 |
+
"flags": {},
|
| 19 |
+
"order": 0,
|
| 20 |
+
"mode": 0,
|
| 21 |
+
"inputs": [],
|
| 22 |
+
"outputs": [],
|
| 23 |
+
"properties": {
|
| 24 |
+
"Node name for S&R": "Note"
|
| 25 |
+
},
|
| 26 |
+
"widgets_values": [
|
| 27 |
+
"CALIBRATED QUANTIZE - measure your own activations\n\nYOU PROBABLY DO NOT NEED THIS GRAPH.\nCalibration files for stock Krea 2 Turbo and base are published at\nhuggingface.co/AlperKTS/Krea-2-SVDQuant-ComfyUI/tree/main/calibration\n- 6.67 MB each. Download the one matching your variant, drop it in\nComfyUI/output/, name it in the act_stats box of the plain\nkrea2_quantize.json graph, and you get the identical quality lever\nwithout running a sampling pass.\n\nThis graph is for the case those files do not cover: a finetune, a\nmerge, a model whose weights are not the released ones. Activation\nstatistics describe the WEIGHTS they were captured from. Same\narchitecture is not the same model - Turbo's file loads cleanly into\na base build and quietly describes the wrong thing.\n\nWHAT IT BUYS\nThe low-rank branch is fitted against activations measured from a\nreal sampling pass instead of assuming every input channel matters\nequally. Measured, no LoRA, rank 64: LPIPS to BF16 0.3378 -> 0.2825.\nSame rank, same file size, same kernel, same speed.\n\nWHAT RUNS, IN ORDER\n1. The BF16 model is loaded with the stock UNETLoader and sampled\n once. Capture Start hooks every layer that will be quantized and\n records the RMS of its inputs; Capture Save writes them out.\n2. Capture Save's act_stats_path feeds the Quantize node. That wire\n is not cosmetic - it is what stops ComfyUI running the quantizer\n first, which would silently produce an uncalibrated file.\n\nSo one Queue press does calibration AND quantization. Expect the\nsampling pass plus 54s-6min of quantizing, with the queue blocked.\n\nSET THIS BEFORE YOU RUN\n- UNETLoader: the BF16 checkpoint. Statistics from an already\n quantized model describe the wrong thing.\n- Quantize node: the SAME file as source_model. It is a separate\n dropdown, and no, it cannot be wired - the loader hands over a\n MODEL object, not a filename.\n- variant: turbo or base, so the output gets the right name.\n- KSampler: 20 steps / cfg 3.5 suits base. For Turbo use 8 steps\n and cfg 1.0, and swap the negative prompt for ConditioningZeroOut.\n\nCALIBRATING ON SEVERAL PROMPTS (better, optional)\nThe defaults calibrate on one prompt, which is enough to beat no\ncalibration. To use more: set keep_capturing = true on Capture Save,\nmute the Quantize node (select it, Ctrl+M), queue as many prompts as\nyou like with reset = false on Capture Start after the first, then\nunmute Quantize and queue once more. Prompts should look like what\nyou actually generate.\n\nOnly svdq uses act_stats. The other formats have no branch to fit,\nand wiring act_stats into them is an error rather than a no-op."
|
| 28 |
+
],
|
| 29 |
+
"title": "READ ME FIRST",
|
| 30 |
+
"color": "#432",
|
| 31 |
+
"bgcolor": "#653"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": 2,
|
| 35 |
+
"type": "UNETLoader",
|
| 36 |
+
"pos": [
|
| 37 |
+
480,
|
| 38 |
+
60
|
| 39 |
+
],
|
| 40 |
+
"size": [
|
| 41 |
+
400,
|
| 42 |
+
120
|
| 43 |
+
],
|
| 44 |
+
"flags": {},
|
| 45 |
+
"order": 1,
|
| 46 |
+
"mode": 0,
|
| 47 |
+
"inputs": [],
|
| 48 |
+
"outputs": [
|
| 49 |
+
{
|
| 50 |
+
"name": "MODEL",
|
| 51 |
+
"type": "MODEL",
|
| 52 |
+
"links": [
|
| 53 |
+
1
|
| 54 |
+
],
|
| 55 |
+
"slot_index": 0
|
| 56 |
+
}
|
| 57 |
+
],
|
| 58 |
+
"properties": {
|
| 59 |
+
"Node name for S&R": "UNETLoader"
|
| 60 |
+
},
|
| 61 |
+
"widgets_values": [
|
| 62 |
+
"krea2_bf16.safetensors",
|
| 63 |
+
"default"
|
| 64 |
+
],
|
| 65 |
+
"title": "BF16 source (stock loader)",
|
| 66 |
+
"color": "#233",
|
| 67 |
+
"bgcolor": "#355"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"id": 3,
|
| 71 |
+
"type": "CLIPLoader",
|
| 72 |
+
"pos": [
|
| 73 |
+
480,
|
| 74 |
+
260
|
| 75 |
+
],
|
| 76 |
+
"size": [
|
| 77 |
+
400,
|
| 78 |
+
120
|
| 79 |
+
],
|
| 80 |
+
"flags": {},
|
| 81 |
+
"order": 2,
|
| 82 |
+
"mode": 0,
|
| 83 |
+
"inputs": [],
|
| 84 |
+
"outputs": [
|
| 85 |
+
{
|
| 86 |
+
"name": "CLIP",
|
| 87 |
+
"type": "CLIP",
|
| 88 |
+
"links": [
|
| 89 |
+
3,
|
| 90 |
+
4
|
| 91 |
+
],
|
| 92 |
+
"slot_index": 0
|
| 93 |
+
}
|
| 94 |
+
],
|
| 95 |
+
"properties": {
|
| 96 |
+
"Node name for S&R": "CLIPLoader"
|
| 97 |
+
},
|
| 98 |
+
"widgets_values": [
|
| 99 |
+
"qwen3vl_4b_fp8_scaled.safetensors",
|
| 100 |
+
"krea2",
|
| 101 |
+
"default"
|
| 102 |
+
],
|
| 103 |
+
"title": "Text encoder (Qwen3-VL 4B)",
|
| 104 |
+
"color": "#233",
|
| 105 |
+
"bgcolor": "#355"
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"id": 4,
|
| 109 |
+
"type": "Krea2SVDQuantCaptureStart",
|
| 110 |
+
"pos": [
|
| 111 |
+
900,
|
| 112 |
+
60
|
| 113 |
+
],
|
| 114 |
+
"size": [
|
| 115 |
+
400,
|
| 116 |
+
100
|
| 117 |
+
],
|
| 118 |
+
"flags": {},
|
| 119 |
+
"order": 3,
|
| 120 |
+
"mode": 0,
|
| 121 |
+
"inputs": [
|
| 122 |
+
{
|
| 123 |
+
"name": "model",
|
| 124 |
+
"type": "MODEL",
|
| 125 |
+
"link": 1
|
| 126 |
+
}
|
| 127 |
+
],
|
| 128 |
+
"outputs": [
|
| 129 |
+
{
|
| 130 |
+
"name": "model",
|
| 131 |
+
"type": "MODEL",
|
| 132 |
+
"links": [
|
| 133 |
+
2
|
| 134 |
+
],
|
| 135 |
+
"slot_index": 0
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"name": "status",
|
| 139 |
+
"type": "STRING",
|
| 140 |
+
"links": [],
|
| 141 |
+
"slot_index": 1
|
| 142 |
+
}
|
| 143 |
+
],
|
| 144 |
+
"properties": {
|
| 145 |
+
"Node name for S&R": "Krea2SVDQuantCaptureStart"
|
| 146 |
+
},
|
| 147 |
+
"widgets_values": [
|
| 148 |
+
true
|
| 149 |
+
],
|
| 150 |
+
"title": "Capture Start - before the sampler",
|
| 151 |
+
"color": "#232",
|
| 152 |
+
"bgcolor": "#353"
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"id": 5,
|
| 156 |
+
"type": "CLIPTextEncode",
|
| 157 |
+
"pos": [
|
| 158 |
+
900,
|
| 159 |
+
220.0
|
| 160 |
+
],
|
| 161 |
+
"size": [
|
| 162 |
+
400,
|
| 163 |
+
200
|
| 164 |
+
],
|
| 165 |
+
"flags": {},
|
| 166 |
+
"order": 4,
|
| 167 |
+
"mode": 0,
|
| 168 |
+
"inputs": [
|
| 169 |
+
{
|
| 170 |
+
"name": "clip",
|
| 171 |
+
"type": "CLIP",
|
| 172 |
+
"link": 3
|
| 173 |
+
}
|
| 174 |
+
],
|
| 175 |
+
"outputs": [
|
| 176 |
+
{
|
| 177 |
+
"name": "CONDITIONING",
|
| 178 |
+
"type": "CONDITIONING",
|
| 179 |
+
"links": [
|
| 180 |
+
5
|
| 181 |
+
],
|
| 182 |
+
"slot_index": 0
|
| 183 |
+
}
|
| 184 |
+
],
|
| 185 |
+
"properties": {
|
| 186 |
+
"Node name for S&R": "CLIPTextEncode"
|
| 187 |
+
},
|
| 188 |
+
"widgets_values": [
|
| 189 |
+
"A cluttered antique clockmaker's workshop seen through a cracked magnifying glass, brass gears laid out in a spiral on the workbench, a ginger cat asleep on a stack of leather-bound books, warm afternoon light with visible dust motes, photorealistic, 85mm lens, shallow depth of field"
|
| 190 |
+
],
|
| 191 |
+
"title": "Calibration prompt",
|
| 192 |
+
"color": "#232",
|
| 193 |
+
"bgcolor": "#353"
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"id": 6,
|
| 197 |
+
"type": "CLIPTextEncode",
|
| 198 |
+
"pos": [
|
| 199 |
+
900,
|
| 200 |
+
460.0
|
| 201 |
+
],
|
| 202 |
+
"size": [
|
| 203 |
+
400,
|
| 204 |
+
140
|
| 205 |
+
],
|
| 206 |
+
"flags": {},
|
| 207 |
+
"order": 5,
|
| 208 |
+
"mode": 0,
|
| 209 |
+
"inputs": [
|
| 210 |
+
{
|
| 211 |
+
"name": "clip",
|
| 212 |
+
"type": "CLIP",
|
| 213 |
+
"link": 4
|
| 214 |
+
}
|
| 215 |
+
],
|
| 216 |
+
"outputs": [
|
| 217 |
+
{
|
| 218 |
+
"name": "CONDITIONING",
|
| 219 |
+
"type": "CONDITIONING",
|
| 220 |
+
"links": [
|
| 221 |
+
6
|
| 222 |
+
],
|
| 223 |
+
"slot_index": 0
|
| 224 |
+
}
|
| 225 |
+
],
|
| 226 |
+
"properties": {
|
| 227 |
+
"Node name for S&R": "CLIPTextEncode"
|
| 228 |
+
},
|
| 229 |
+
"widgets_values": [
|
| 230 |
+
"blurry, low resolution, jpeg artifacts, watermark, deformed, extra limbs"
|
| 231 |
+
],
|
| 232 |
+
"title": "Negative (Turbo: replace with ConditioningZeroOut)",
|
| 233 |
+
"color": "#232",
|
| 234 |
+
"bgcolor": "#353"
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
"id": 7,
|
| 238 |
+
"type": "EmptySD3LatentImage",
|
| 239 |
+
"pos": [
|
| 240 |
+
900,
|
| 241 |
+
660.0
|
| 242 |
+
],
|
| 243 |
+
"size": [
|
| 244 |
+
400,
|
| 245 |
+
120
|
| 246 |
+
],
|
| 247 |
+
"flags": {},
|
| 248 |
+
"order": 6,
|
| 249 |
+
"mode": 0,
|
| 250 |
+
"inputs": [],
|
| 251 |
+
"outputs": [
|
| 252 |
+
{
|
| 253 |
+
"name": "LATENT",
|
| 254 |
+
"type": "LATENT",
|
| 255 |
+
"links": [
|
| 256 |
+
7
|
| 257 |
+
],
|
| 258 |
+
"slot_index": 0
|
| 259 |
+
}
|
| 260 |
+
],
|
| 261 |
+
"properties": {
|
| 262 |
+
"Node name for S&R": "EmptySD3LatentImage"
|
| 263 |
+
},
|
| 264 |
+
"widgets_values": [
|
| 265 |
+
1024,
|
| 266 |
+
1024,
|
| 267 |
+
1
|
| 268 |
+
],
|
| 269 |
+
"title": "Latent 1024x1024",
|
| 270 |
+
"color": "#232",
|
| 271 |
+
"bgcolor": "#353"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"id": 8,
|
| 275 |
+
"type": "KSampler",
|
| 276 |
+
"pos": [
|
| 277 |
+
1320,
|
| 278 |
+
60
|
| 279 |
+
],
|
| 280 |
+
"size": [
|
| 281 |
+
400,
|
| 282 |
+
280
|
| 283 |
+
],
|
| 284 |
+
"flags": {},
|
| 285 |
+
"order": 7,
|
| 286 |
+
"mode": 0,
|
| 287 |
+
"inputs": [
|
| 288 |
+
{
|
| 289 |
+
"name": "model",
|
| 290 |
+
"type": "MODEL",
|
| 291 |
+
"link": 2
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
"name": "positive",
|
| 295 |
+
"type": "CONDITIONING",
|
| 296 |
+
"link": 5
|
| 297 |
+
},
|
| 298 |
+
{
|
| 299 |
+
"name": "negative",
|
| 300 |
+
"type": "CONDITIONING",
|
| 301 |
+
"link": 6
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
"name": "latent_image",
|
| 305 |
+
"type": "LATENT",
|
| 306 |
+
"link": 7
|
| 307 |
+
}
|
| 308 |
+
],
|
| 309 |
+
"outputs": [
|
| 310 |
+
{
|
| 311 |
+
"name": "LATENT",
|
| 312 |
+
"type": "LATENT",
|
| 313 |
+
"links": [
|
| 314 |
+
8
|
| 315 |
+
],
|
| 316 |
+
"slot_index": 0
|
| 317 |
+
}
|
| 318 |
+
],
|
| 319 |
+
"properties": {
|
| 320 |
+
"Node name for S&R": "KSampler"
|
| 321 |
+
},
|
| 322 |
+
"widgets_values": [
|
| 323 |
+
987654321,
|
| 324 |
+
"randomize",
|
| 325 |
+
20,
|
| 326 |
+
3.5,
|
| 327 |
+
"euler",
|
| 328 |
+
"simple",
|
| 329 |
+
1.0
|
| 330 |
+
],
|
| 331 |
+
"title": "Calibration pass - base 20/3.5, Turbo 8/1.0",
|
| 332 |
+
"color": "#323",
|
| 333 |
+
"bgcolor": "#535"
|
| 334 |
+
},
|
| 335 |
+
{
|
| 336 |
+
"id": 9,
|
| 337 |
+
"type": "Krea2SVDQuantCaptureSave",
|
| 338 |
+
"pos": [
|
| 339 |
+
1740,
|
| 340 |
+
60
|
| 341 |
+
],
|
| 342 |
+
"size": [
|
| 343 |
+
400,
|
| 344 |
+
120
|
| 345 |
+
],
|
| 346 |
+
"flags": {},
|
| 347 |
+
"order": 8,
|
| 348 |
+
"mode": 0,
|
| 349 |
+
"inputs": [
|
| 350 |
+
{
|
| 351 |
+
"name": "latent",
|
| 352 |
+
"type": "LATENT",
|
| 353 |
+
"link": 8
|
| 354 |
+
}
|
| 355 |
+
],
|
| 356 |
+
"outputs": [
|
| 357 |
+
{
|
| 358 |
+
"name": "latent",
|
| 359 |
+
"type": "LATENT",
|
| 360 |
+
"links": [],
|
| 361 |
+
"slot_index": 0
|
| 362 |
+
},
|
| 363 |
+
{
|
| 364 |
+
"name": "status",
|
| 365 |
+
"type": "STRING",
|
| 366 |
+
"links": [],
|
| 367 |
+
"slot_index": 1
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"name": "act_stats_path",
|
| 371 |
+
"type": "STRING",
|
| 372 |
+
"links": [
|
| 373 |
+
9
|
| 374 |
+
],
|
| 375 |
+
"slot_index": 2
|
| 376 |
+
}
|
| 377 |
+
],
|
| 378 |
+
"properties": {
|
| 379 |
+
"Node name for S&R": "Krea2SVDQuantCaptureSave"
|
| 380 |
+
},
|
| 381 |
+
"widgets_values": [
|
| 382 |
+
"krea2_act_stats.safetensors",
|
| 383 |
+
false
|
| 384 |
+
],
|
| 385 |
+
"title": "Capture Save - after the sampler",
|
| 386 |
+
"color": "#232",
|
| 387 |
+
"bgcolor": "#353"
|
| 388 |
+
},
|
| 389 |
+
{
|
| 390 |
+
"id": 10,
|
| 391 |
+
"type": "Krea2SVDQuantQuantize",
|
| 392 |
+
"pos": [
|
| 393 |
+
2160,
|
| 394 |
+
60
|
| 395 |
+
],
|
| 396 |
+
"size": [
|
| 397 |
+
420,
|
| 398 |
+
440
|
| 399 |
+
],
|
| 400 |
+
"flags": {},
|
| 401 |
+
"order": 9,
|
| 402 |
+
"mode": 0,
|
| 403 |
+
"inputs": [
|
| 404 |
+
{
|
| 405 |
+
"name": "act_stats",
|
| 406 |
+
"type": "STRING",
|
| 407 |
+
"link": 9,
|
| 408 |
+
"widget": {
|
| 409 |
+
"name": "act_stats"
|
| 410 |
+
}
|
| 411 |
+
}
|
| 412 |
+
],
|
| 413 |
+
"outputs": [
|
| 414 |
+
{
|
| 415 |
+
"name": "summary",
|
| 416 |
+
"type": "STRING",
|
| 417 |
+
"links": [],
|
| 418 |
+
"slot_index": 0
|
| 419 |
+
}
|
| 420 |
+
],
|
| 421 |
+
"properties": {
|
| 422 |
+
"Node name for S&R": "Krea2SVDQuantQuantize"
|
| 423 |
+
},
|
| 424 |
+
"widgets_values": [
|
| 425 |
+
"krea2_bf16.safetensors",
|
| 426 |
+
"svdq",
|
| 427 |
+
64,
|
| 428 |
+
"uniform",
|
| 429 |
+
100,
|
| 430 |
+
256,
|
| 431 |
+
"turbo",
|
| 432 |
+
"",
|
| 433 |
+
false,
|
| 434 |
+
"",
|
| 435 |
+
0
|
| 436 |
+
],
|
| 437 |
+
"title": "Quantize - source_model must match the loader above",
|
| 438 |
+
"color": "#233",
|
| 439 |
+
"bgcolor": "#355"
|
| 440 |
+
}
|
| 441 |
+
],
|
| 442 |
+
"links": [
|
| 443 |
+
[
|
| 444 |
+
1,
|
| 445 |
+
2,
|
| 446 |
+
0,
|
| 447 |
+
4,
|
| 448 |
+
0,
|
| 449 |
+
"MODEL"
|
| 450 |
+
],
|
| 451 |
+
[
|
| 452 |
+
2,
|
| 453 |
+
4,
|
| 454 |
+
0,
|
| 455 |
+
8,
|
| 456 |
+
0,
|
| 457 |
+
"MODEL"
|
| 458 |
+
],
|
| 459 |
+
[
|
| 460 |
+
3,
|
| 461 |
+
3,
|
| 462 |
+
0,
|
| 463 |
+
5,
|
| 464 |
+
0,
|
| 465 |
+
"CLIP"
|
| 466 |
+
],
|
| 467 |
+
[
|
| 468 |
+
4,
|
| 469 |
+
3,
|
| 470 |
+
0,
|
| 471 |
+
6,
|
| 472 |
+
0,
|
| 473 |
+
"CLIP"
|
| 474 |
+
],
|
| 475 |
+
[
|
| 476 |
+
5,
|
| 477 |
+
5,
|
| 478 |
+
0,
|
| 479 |
+
8,
|
| 480 |
+
1,
|
| 481 |
+
"CONDITIONING"
|
| 482 |
+
],
|
| 483 |
+
[
|
| 484 |
+
6,
|
| 485 |
+
6,
|
| 486 |
+
0,
|
| 487 |
+
8,
|
| 488 |
+
2,
|
| 489 |
+
"CONDITIONING"
|
| 490 |
+
],
|
| 491 |
+
[
|
| 492 |
+
7,
|
| 493 |
+
7,
|
| 494 |
+
0,
|
| 495 |
+
8,
|
| 496 |
+
3,
|
| 497 |
+
"LATENT"
|
| 498 |
+
],
|
| 499 |
+
[
|
| 500 |
+
8,
|
| 501 |
+
8,
|
| 502 |
+
0,
|
| 503 |
+
9,
|
| 504 |
+
0,
|
| 505 |
+
"LATENT"
|
| 506 |
+
],
|
| 507 |
+
[
|
| 508 |
+
9,
|
| 509 |
+
9,
|
| 510 |
+
2,
|
| 511 |
+
10,
|
| 512 |
+
0,
|
| 513 |
+
"STRING"
|
| 514 |
+
]
|
| 515 |
+
],
|
| 516 |
+
"groups": [
|
| 517 |
+
{
|
| 518 |
+
"title": "Load BF16",
|
| 519 |
+
"bounding": [
|
| 520 |
+
500,
|
| 521 |
+
20,
|
| 522 |
+
420,
|
| 523 |
+
420
|
| 524 |
+
],
|
| 525 |
+
"color": "#3f789e",
|
| 526 |
+
"font_size": 24,
|
| 527 |
+
"flags": {}
|
| 528 |
+
},
|
| 529 |
+
{
|
| 530 |
+
"title": "Calibration pass",
|
| 531 |
+
"bounding": [
|
| 532 |
+
940,
|
| 533 |
+
20,
|
| 534 |
+
420,
|
| 535 |
+
800
|
| 536 |
+
],
|
| 537 |
+
"color": "#3f789e",
|
| 538 |
+
"font_size": 24,
|
| 539 |
+
"flags": {}
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"title": "Quantize",
|
| 543 |
+
"bounding": [
|
| 544 |
+
2200,
|
| 545 |
+
20,
|
| 546 |
+
460,
|
| 547 |
+
520
|
| 548 |
+
],
|
| 549 |
+
"color": "#8a4",
|
| 550 |
+
"font_size": 24,
|
| 551 |
+
"flags": {}
|
| 552 |
+
}
|
| 553 |
+
],
|
| 554 |
+
"config": {},
|
| 555 |
+
"extra": {},
|
| 556 |
+
"version": 0.4
|
| 557 |
+
}
|
workflows/krea2_quantize_calibrated_api.json
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"1": {
|
| 3 |
+
"class_type": "UNETLoader",
|
| 4 |
+
"inputs": {
|
| 5 |
+
"unet_name": "krea2_bf16.safetensors",
|
| 6 |
+
"weight_dtype": "default"
|
| 7 |
+
},
|
| 8 |
+
"_meta": {
|
| 9 |
+
"title": "BF16 source (stock loader)"
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"2": {
|
| 13 |
+
"class_type": "CLIPLoader",
|
| 14 |
+
"inputs": {
|
| 15 |
+
"clip_name": "qwen3vl_4b_fp8_scaled.safetensors",
|
| 16 |
+
"type": "krea2",
|
| 17 |
+
"device": "default"
|
| 18 |
+
},
|
| 19 |
+
"_meta": {
|
| 20 |
+
"title": "Text encoder (Qwen3-VL 4B)"
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"3": {
|
| 24 |
+
"class_type": "Krea2SVDQuantCaptureStart",
|
| 25 |
+
"inputs": {
|
| 26 |
+
"reset": true,
|
| 27 |
+
"model": [
|
| 28 |
+
"1",
|
| 29 |
+
0
|
| 30 |
+
]
|
| 31 |
+
},
|
| 32 |
+
"_meta": {
|
| 33 |
+
"title": "Capture Start - before the sampler"
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"4": {
|
| 37 |
+
"class_type": "CLIPTextEncode",
|
| 38 |
+
"inputs": {
|
| 39 |
+
"text": "A cluttered antique clockmaker's workshop seen through a cracked magnifying glass, brass gears laid out in a spiral on the workbench, a ginger cat asleep on a stack of leather-bound books, warm afternoon light with visible dust motes, photorealistic, 85mm lens, shallow depth of field",
|
| 40 |
+
"clip": [
|
| 41 |
+
"2",
|
| 42 |
+
0
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
+
"_meta": {
|
| 46 |
+
"title": "Calibration prompt"
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
"5": {
|
| 50 |
+
"class_type": "CLIPTextEncode",
|
| 51 |
+
"inputs": {
|
| 52 |
+
"text": "blurry, low resolution, jpeg artifacts, watermark, deformed, extra limbs",
|
| 53 |
+
"clip": [
|
| 54 |
+
"2",
|
| 55 |
+
0
|
| 56 |
+
]
|
| 57 |
+
},
|
| 58 |
+
"_meta": {
|
| 59 |
+
"title": "Negative (Turbo: replace with ConditioningZeroOut)"
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
"6": {
|
| 63 |
+
"class_type": "EmptySD3LatentImage",
|
| 64 |
+
"inputs": {
|
| 65 |
+
"width": 1024,
|
| 66 |
+
"height": 1024,
|
| 67 |
+
"batch_size": 1
|
| 68 |
+
},
|
| 69 |
+
"_meta": {
|
| 70 |
+
"title": "Latent 1024x1024"
|
| 71 |
+
}
|
| 72 |
+
},
|
| 73 |
+
"7": {
|
| 74 |
+
"class_type": "KSampler",
|
| 75 |
+
"inputs": {
|
| 76 |
+
"seed": 987654321,
|
| 77 |
+
"steps": 20,
|
| 78 |
+
"cfg": 3.5,
|
| 79 |
+
"sampler_name": "euler",
|
| 80 |
+
"scheduler": "simple",
|
| 81 |
+
"denoise": 1.0,
|
| 82 |
+
"model": [
|
| 83 |
+
"3",
|
| 84 |
+
0
|
| 85 |
+
],
|
| 86 |
+
"positive": [
|
| 87 |
+
"4",
|
| 88 |
+
0
|
| 89 |
+
],
|
| 90 |
+
"negative": [
|
| 91 |
+
"5",
|
| 92 |
+
0
|
| 93 |
+
],
|
| 94 |
+
"latent_image": [
|
| 95 |
+
"6",
|
| 96 |
+
0
|
| 97 |
+
]
|
| 98 |
+
},
|
| 99 |
+
"_meta": {
|
| 100 |
+
"title": "Calibration pass - base 20/3.5, Turbo 8/1.0"
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
"8": {
|
| 104 |
+
"class_type": "Krea2SVDQuantCaptureSave",
|
| 105 |
+
"inputs": {
|
| 106 |
+
"filename": "krea2_act_stats.safetensors",
|
| 107 |
+
"keep_capturing": false,
|
| 108 |
+
"latent": [
|
| 109 |
+
"7",
|
| 110 |
+
0
|
| 111 |
+
]
|
| 112 |
+
},
|
| 113 |
+
"_meta": {
|
| 114 |
+
"title": "Capture Save - after the sampler"
|
| 115 |
+
}
|
| 116 |
+
},
|
| 117 |
+
"9": {
|
| 118 |
+
"class_type": "Krea2SVDQuantQuantize",
|
| 119 |
+
"inputs": {
|
| 120 |
+
"source_model": "krea2_bf16.safetensors",
|
| 121 |
+
"format": "svdq",
|
| 122 |
+
"rank": 64,
|
| 123 |
+
"rank_alloc": "uniform",
|
| 124 |
+
"refine_iters": 100,
|
| 125 |
+
"groupsize": 256,
|
| 126 |
+
"variant": "turbo",
|
| 127 |
+
"output_name": "",
|
| 128 |
+
"overwrite": false,
|
| 129 |
+
"act_stats": [
|
| 130 |
+
"8",
|
| 131 |
+
2
|
| 132 |
+
],
|
| 133 |
+
"seed": 0
|
| 134 |
+
},
|
| 135 |
+
"_meta": {
|
| 136 |
+
"title": "Quantize - source_model must match the loader above"
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
}
|
workflows/krea2_svdquant_diagnostics.json
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "00000000-0000-4000-8000-000000000000",
|
| 3 |
+
"revision": 0,
|
| 4 |
+
"last_node_id": 7,
|
| 5 |
+
"last_link_id": 4,
|
| 6 |
+
"nodes": [
|
| 7 |
+
{
|
| 8 |
+
"id": 1,
|
| 9 |
+
"type": "Note",
|
| 10 |
+
"pos": [
|
| 11 |
+
60,
|
| 12 |
+
60
|
| 13 |
+
],
|
| 14 |
+
"size": [
|
| 15 |
+
430,
|
| 16 |
+
700
|
| 17 |
+
],
|
| 18 |
+
"flags": {},
|
| 19 |
+
"order": 0,
|
| 20 |
+
"mode": 0,
|
| 21 |
+
"inputs": [],
|
| 22 |
+
"outputs": [],
|
| 23 |
+
"properties": {
|
| 24 |
+
"Node name for S&R": "Note"
|
| 25 |
+
},
|
| 26 |
+
"widgets_values": [
|
| 27 |
+
"KREA 2 SVDQUANT - DIAGNOSTICS\n\nRun this before reporting a bug, and before downloading 8 GB.\nNothing here generates an image.\n\nENV CHECK (left, no model needed)\n Answers the single most common report: \"the quantized checkpoint\n is slower than fp8\". Almost always the cause is that ComfyUI\n disabled comfy_kitchen's CUDA backend because torch was built\n against CUDA < 13, and the fallback dequantizes int4 in Python --\n slower than bf16, not faster. Needs no model, so run it first.\n\nDIAGNOSTICS (right, needs a checkpoint) - the mode widget:\n dispatch which kernel actually runs. START HERE.\n env versions + memory accounting, including where the\n low-rank factors currently live. Under lowvram they\n should read cpu between steps, not cuda.\n bench quantized vs bf16 timing per layer.\n profile torch.profiler table.\n compile graph breaks a TorchCompileModel run would pay.\n\n tokens = sequence length. 4096 is 1024x1024. Kernel selection is\n shape-dependent, so match your real resolution.\n\nWHAT TO PASTE INTO AN ISSUE\n Your GPU, the checkpoint filename, and the report output of both\n nodes. Both are OUTPUT_NODEs, so the text shows in the node itself\n and in the console. Neither is cached -- they re-measure the live\n process every run, which is the point."
|
| 28 |
+
],
|
| 29 |
+
"title": "READ ME FIRST",
|
| 30 |
+
"color": "#432",
|
| 31 |
+
"bgcolor": "#653"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": 2,
|
| 35 |
+
"type": "Krea2SVDQuantEnvCheck",
|
| 36 |
+
"pos": [
|
| 37 |
+
480,
|
| 38 |
+
60
|
| 39 |
+
],
|
| 40 |
+
"size": [
|
| 41 |
+
400,
|
| 42 |
+
80
|
| 43 |
+
],
|
| 44 |
+
"flags": {},
|
| 45 |
+
"order": 1,
|
| 46 |
+
"mode": 0,
|
| 47 |
+
"inputs": [],
|
| 48 |
+
"outputs": [
|
| 49 |
+
{
|
| 50 |
+
"name": "report",
|
| 51 |
+
"type": "STRING",
|
| 52 |
+
"links": [
|
| 53 |
+
1
|
| 54 |
+
],
|
| 55 |
+
"slot_index": 0
|
| 56 |
+
}
|
| 57 |
+
],
|
| 58 |
+
"properties": {
|
| 59 |
+
"Node name for S&R": "Krea2SVDQuantEnvCheck"
|
| 60 |
+
},
|
| 61 |
+
"widgets_values": [],
|
| 62 |
+
"title": "1. Env Check - no model needed, run this first",
|
| 63 |
+
"color": "#432",
|
| 64 |
+
"bgcolor": "#653"
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"id": 3,
|
| 68 |
+
"type": "PreviewAny",
|
| 69 |
+
"pos": [
|
| 70 |
+
480,
|
| 71 |
+
180.0
|
| 72 |
+
],
|
| 73 |
+
"size": [
|
| 74 |
+
400,
|
| 75 |
+
300
|
| 76 |
+
],
|
| 77 |
+
"flags": {},
|
| 78 |
+
"order": 2,
|
| 79 |
+
"mode": 0,
|
| 80 |
+
"inputs": [
|
| 81 |
+
{
|
| 82 |
+
"name": "source",
|
| 83 |
+
"type": "*",
|
| 84 |
+
"link": 1
|
| 85 |
+
}
|
| 86 |
+
],
|
| 87 |
+
"outputs": [],
|
| 88 |
+
"properties": {
|
| 89 |
+
"Node name for S&R": "PreviewAny"
|
| 90 |
+
},
|
| 91 |
+
"widgets_values": [],
|
| 92 |
+
"title": "Env Check report",
|
| 93 |
+
"color": "#432",
|
| 94 |
+
"bgcolor": "#653"
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"id": 4,
|
| 98 |
+
"type": "Krea2SVDQuantW4A4Loader",
|
| 99 |
+
"pos": [
|
| 100 |
+
900,
|
| 101 |
+
60
|
| 102 |
+
],
|
| 103 |
+
"size": [
|
| 104 |
+
400,
|
| 105 |
+
120
|
| 106 |
+
],
|
| 107 |
+
"flags": {},
|
| 108 |
+
"order": 3,
|
| 109 |
+
"mode": 0,
|
| 110 |
+
"inputs": [],
|
| 111 |
+
"outputs": [
|
| 112 |
+
{
|
| 113 |
+
"name": "MODEL",
|
| 114 |
+
"type": "MODEL",
|
| 115 |
+
"links": [
|
| 116 |
+
3
|
| 117 |
+
],
|
| 118 |
+
"slot_index": 0
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"name": "STRING",
|
| 122 |
+
"type": "STRING",
|
| 123 |
+
"links": [
|
| 124 |
+
2
|
| 125 |
+
],
|
| 126 |
+
"slot_index": 1
|
| 127 |
+
}
|
| 128 |
+
],
|
| 129 |
+
"properties": {
|
| 130 |
+
"Node name for S&R": "Krea2SVDQuantW4A4Loader"
|
| 131 |
+
},
|
| 132 |
+
"widgets_values": [
|
| 133 |
+
"Krea2-Turbo-SVDQuant-W4A4-rank256-actaware.safetensors"
|
| 134 |
+
],
|
| 135 |
+
"title": "2. Load the checkpoint you are diagnosing",
|
| 136 |
+
"color": "#233",
|
| 137 |
+
"bgcolor": "#355"
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
"id": 5,
|
| 141 |
+
"type": "PreviewAny",
|
| 142 |
+
"pos": [
|
| 143 |
+
900,
|
| 144 |
+
200.0
|
| 145 |
+
],
|
| 146 |
+
"size": [
|
| 147 |
+
400,
|
| 148 |
+
200
|
| 149 |
+
],
|
| 150 |
+
"flags": {},
|
| 151 |
+
"order": 4,
|
| 152 |
+
"mode": 0,
|
| 153 |
+
"inputs": [
|
| 154 |
+
{
|
| 155 |
+
"name": "source",
|
| 156 |
+
"type": "*",
|
| 157 |
+
"link": 2
|
| 158 |
+
}
|
| 159 |
+
],
|
| 160 |
+
"outputs": [],
|
| 161 |
+
"properties": {
|
| 162 |
+
"Node name for S&R": "PreviewAny"
|
| 163 |
+
},
|
| 164 |
+
"widgets_values": [],
|
| 165 |
+
"title": "Loader status - names the kernel that will run",
|
| 166 |
+
"color": "#233",
|
| 167 |
+
"bgcolor": "#355"
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"id": 6,
|
| 171 |
+
"type": "Krea2SVDQuantDiagnostics",
|
| 172 |
+
"pos": [
|
| 173 |
+
1320,
|
| 174 |
+
60
|
| 175 |
+
],
|
| 176 |
+
"size": [
|
| 177 |
+
400,
|
| 178 |
+
130
|
| 179 |
+
],
|
| 180 |
+
"flags": {},
|
| 181 |
+
"order": 5,
|
| 182 |
+
"mode": 0,
|
| 183 |
+
"inputs": [
|
| 184 |
+
{
|
| 185 |
+
"name": "model",
|
| 186 |
+
"type": "MODEL",
|
| 187 |
+
"link": 3
|
| 188 |
+
}
|
| 189 |
+
],
|
| 190 |
+
"outputs": [
|
| 191 |
+
{
|
| 192 |
+
"name": "model",
|
| 193 |
+
"type": "MODEL",
|
| 194 |
+
"links": [],
|
| 195 |
+
"slot_index": 0
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"name": "report",
|
| 199 |
+
"type": "STRING",
|
| 200 |
+
"links": [
|
| 201 |
+
4
|
| 202 |
+
],
|
| 203 |
+
"slot_index": 1
|
| 204 |
+
}
|
| 205 |
+
],
|
| 206 |
+
"properties": {
|
| 207 |
+
"Node name for S&R": "Krea2SVDQuantDiagnostics"
|
| 208 |
+
},
|
| 209 |
+
"widgets_values": [
|
| 210 |
+
"dispatch",
|
| 211 |
+
4096
|
| 212 |
+
],
|
| 213 |
+
"title": "3. Diagnostics - start with mode=dispatch",
|
| 214 |
+
"color": "#432",
|
| 215 |
+
"bgcolor": "#653"
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"id": 7,
|
| 219 |
+
"type": "PreviewAny",
|
| 220 |
+
"pos": [
|
| 221 |
+
1320,
|
| 222 |
+
220.0
|
| 223 |
+
],
|
| 224 |
+
"size": [
|
| 225 |
+
400,
|
| 226 |
+
400
|
| 227 |
+
],
|
| 228 |
+
"flags": {},
|
| 229 |
+
"order": 6,
|
| 230 |
+
"mode": 0,
|
| 231 |
+
"inputs": [
|
| 232 |
+
{
|
| 233 |
+
"name": "source",
|
| 234 |
+
"type": "*",
|
| 235 |
+
"link": 4
|
| 236 |
+
}
|
| 237 |
+
],
|
| 238 |
+
"outputs": [],
|
| 239 |
+
"properties": {
|
| 240 |
+
"Node name for S&R": "PreviewAny"
|
| 241 |
+
},
|
| 242 |
+
"widgets_values": [],
|
| 243 |
+
"title": "Diagnostics report - paste this into an issue",
|
| 244 |
+
"color": "#432",
|
| 245 |
+
"bgcolor": "#653"
|
| 246 |
+
}
|
| 247 |
+
],
|
| 248 |
+
"links": [
|
| 249 |
+
[
|
| 250 |
+
1,
|
| 251 |
+
2,
|
| 252 |
+
0,
|
| 253 |
+
3,
|
| 254 |
+
0,
|
| 255 |
+
"STRING"
|
| 256 |
+
],
|
| 257 |
+
[
|
| 258 |
+
2,
|
| 259 |
+
4,
|
| 260 |
+
1,
|
| 261 |
+
5,
|
| 262 |
+
0,
|
| 263 |
+
"STRING"
|
| 264 |
+
],
|
| 265 |
+
[
|
| 266 |
+
3,
|
| 267 |
+
4,
|
| 268 |
+
0,
|
| 269 |
+
6,
|
| 270 |
+
0,
|
| 271 |
+
"MODEL"
|
| 272 |
+
],
|
| 273 |
+
[
|
| 274 |
+
4,
|
| 275 |
+
6,
|
| 276 |
+
1,
|
| 277 |
+
7,
|
| 278 |
+
0,
|
| 279 |
+
"STRING"
|
| 280 |
+
]
|
| 281 |
+
],
|
| 282 |
+
"groups": [
|
| 283 |
+
{
|
| 284 |
+
"title": "Step 1 - does the int4 kernel exist at all?",
|
| 285 |
+
"bounding": [
|
| 286 |
+
500,
|
| 287 |
+
20,
|
| 288 |
+
420,
|
| 289 |
+
480
|
| 290 |
+
],
|
| 291 |
+
"color": "#a85",
|
| 292 |
+
"font_size": 24,
|
| 293 |
+
"flags": {}
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
"title": "Step 2 - load it",
|
| 297 |
+
"bounding": [
|
| 298 |
+
940,
|
| 299 |
+
20,
|
| 300 |
+
420,
|
| 301 |
+
480
|
| 302 |
+
],
|
| 303 |
+
"color": "#3f789e",
|
| 304 |
+
"font_size": 24,
|
| 305 |
+
"flags": {}
|
| 306 |
+
},
|
| 307 |
+
{
|
| 308 |
+
"title": "Step 3 - what actually runs",
|
| 309 |
+
"bounding": [
|
| 310 |
+
1360,
|
| 311 |
+
20,
|
| 312 |
+
420,
|
| 313 |
+
620
|
| 314 |
+
],
|
| 315 |
+
"color": "#8a4",
|
| 316 |
+
"font_size": 24,
|
| 317 |
+
"flags": {}
|
| 318 |
+
}
|
| 319 |
+
],
|
| 320 |
+
"config": {},
|
| 321 |
+
"extra": {},
|
| 322 |
+
"version": 0.4
|
| 323 |
+
}
|
workflows/krea2_svdquant_diagnostics_api.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"1": {
|
| 3 |
+
"class_type": "Krea2SVDQuantEnvCheck",
|
| 4 |
+
"inputs": {},
|
| 5 |
+
"_meta": {
|
| 6 |
+
"title": "1. Env Check - no model needed, run this first"
|
| 7 |
+
}
|
| 8 |
+
},
|
| 9 |
+
"2": {
|
| 10 |
+
"class_type": "PreviewAny",
|
| 11 |
+
"inputs": {
|
| 12 |
+
"source": [
|
| 13 |
+
"1",
|
| 14 |
+
0
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
"_meta": {
|
| 18 |
+
"title": "Env Check report"
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
"3": {
|
| 22 |
+
"class_type": "Krea2SVDQuantW4A4Loader",
|
| 23 |
+
"inputs": {
|
| 24 |
+
"model_name": "Krea2-Turbo-SVDQuant-W4A4-rank256-actaware.safetensors"
|
| 25 |
+
},
|
| 26 |
+
"_meta": {
|
| 27 |
+
"title": "2. Load the checkpoint you are diagnosing"
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"4": {
|
| 31 |
+
"class_type": "PreviewAny",
|
| 32 |
+
"inputs": {
|
| 33 |
+
"source": [
|
| 34 |
+
"3",
|
| 35 |
+
1
|
| 36 |
+
]
|
| 37 |
+
},
|
| 38 |
+
"_meta": {
|
| 39 |
+
"title": "Loader status - names the kernel that will run"
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
"5": {
|
| 43 |
+
"class_type": "Krea2SVDQuantDiagnostics",
|
| 44 |
+
"inputs": {
|
| 45 |
+
"mode": "dispatch",
|
| 46 |
+
"tokens": 4096,
|
| 47 |
+
"model": [
|
| 48 |
+
"3",
|
| 49 |
+
0
|
| 50 |
+
]
|
| 51 |
+
},
|
| 52 |
+
"_meta": {
|
| 53 |
+
"title": "3. Diagnostics - start with mode=dispatch"
|
| 54 |
+
}
|
| 55 |
+
},
|
| 56 |
+
"6": {
|
| 57 |
+
"class_type": "PreviewAny",
|
| 58 |
+
"inputs": {
|
| 59 |
+
"source": [
|
| 60 |
+
"5",
|
| 61 |
+
1
|
| 62 |
+
]
|
| 63 |
+
},
|
| 64 |
+
"_meta": {
|
| 65 |
+
"title": "Diagnostics report - paste this into an issue"
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
workflows/krea2_turbo_all_in_one_t2i.json
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "00000000-0000-4000-8000-000000000000",
|
| 3 |
+
"revision": 0,
|
| 4 |
+
"last_node_id": 9,
|
| 5 |
+
"last_link_id": 10,
|
| 6 |
+
"nodes": [
|
| 7 |
+
{
|
| 8 |
+
"id": 1,
|
| 9 |
+
"type": "Note",
|
| 10 |
+
"pos": [
|
| 11 |
+
60,
|
| 12 |
+
60
|
| 13 |
+
],
|
| 14 |
+
"size": [
|
| 15 |
+
400,
|
| 16 |
+
620
|
| 17 |
+
],
|
| 18 |
+
"flags": {},
|
| 19 |
+
"order": 0,
|
| 20 |
+
"mode": 0,
|
| 21 |
+
"inputs": [],
|
| 22 |
+
"outputs": [],
|
| 23 |
+
"properties": {
|
| 24 |
+
"Node name for S&R": "Note"
|
| 25 |
+
},
|
| 26 |
+
"widgets_values": [
|
| 27 |
+
"KREA 2 TURBO - ALL-IN-ONE CHECKPOINT\n\n1. Checkpoint goes in ComfyUI/models/checkpoints/.\n This file is fully self-contained: it combines the diffusion model,\n the 4-bit quantized text encoder (Qwen3-VL 4B), and the VAE into a\n single safetensors file (~12 GB instead of 15.5 GB across 3 files).\n\n2. SVDQuant checkpoints (carrying low-rank factors) load with the\n \"Krea2 SVDQuant Checkpoint Loader\" node. Branchless checkpoints\n (--format w4a4 or int8) also load with stock CheckpointLoaderSimple.\n\n3. cfg MUST be 1.0. Krea 2 Turbo is cfg-distilled, so the negative\n prompt is zeroed out (ConditioningZeroOut) rather than encoded.\n 8 steps with euler / simple scheduler is what the model was distilled for.\n\n4. 1024x1024 native resolution. Latent size MUST be multiples of 16."
|
| 28 |
+
],
|
| 29 |
+
"title": "READ ME FIRST",
|
| 30 |
+
"color": "#432",
|
| 31 |
+
"bgcolor": "#653"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": 2,
|
| 35 |
+
"type": "Krea2SVDQuantCheckpointLoader",
|
| 36 |
+
"pos": [
|
| 37 |
+
480,
|
| 38 |
+
60
|
| 39 |
+
],
|
| 40 |
+
"size": [
|
| 41 |
+
400,
|
| 42 |
+
140
|
| 43 |
+
],
|
| 44 |
+
"flags": {},
|
| 45 |
+
"order": 1,
|
| 46 |
+
"mode": 0,
|
| 47 |
+
"inputs": [],
|
| 48 |
+
"outputs": [
|
| 49 |
+
{
|
| 50 |
+
"name": "MODEL",
|
| 51 |
+
"type": "MODEL",
|
| 52 |
+
"links": [
|
| 53 |
+
1,
|
| 54 |
+
10
|
| 55 |
+
],
|
| 56 |
+
"slot_index": 0
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"name": "CLIP",
|
| 60 |
+
"type": "CLIP",
|
| 61 |
+
"links": [
|
| 62 |
+
2
|
| 63 |
+
],
|
| 64 |
+
"slot_index": 1
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"name": "VAE",
|
| 68 |
+
"type": "VAE",
|
| 69 |
+
"links": [
|
| 70 |
+
8
|
| 71 |
+
],
|
| 72 |
+
"slot_index": 2
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"name": "STRING",
|
| 76 |
+
"type": "STRING",
|
| 77 |
+
"links": [],
|
| 78 |
+
"slot_index": 3
|
| 79 |
+
}
|
| 80 |
+
],
|
| 81 |
+
"properties": {
|
| 82 |
+
"Node name for S&R": "Krea2SVDQuantCheckpointLoader"
|
| 83 |
+
},
|
| 84 |
+
"widgets_values": [
|
| 85 |
+
"Krea2-Turbo-AllInOne-SVDQuant-W4A4-rank256-actaware-TEW4A4.safetensors"
|
| 86 |
+
],
|
| 87 |
+
"title": "Krea2 SVDQuant Checkpoint Loader",
|
| 88 |
+
"color": "#233",
|
| 89 |
+
"bgcolor": "#355"
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"id": 3,
|
| 93 |
+
"type": "CLIPTextEncode",
|
| 94 |
+
"pos": [
|
| 95 |
+
900,
|
| 96 |
+
60
|
| 97 |
+
],
|
| 98 |
+
"size": [
|
| 99 |
+
400,
|
| 100 |
+
220
|
| 101 |
+
],
|
| 102 |
+
"flags": {},
|
| 103 |
+
"order": 2,
|
| 104 |
+
"mode": 0,
|
| 105 |
+
"inputs": [
|
| 106 |
+
{
|
| 107 |
+
"name": "clip",
|
| 108 |
+
"type": "CLIP",
|
| 109 |
+
"link": 2
|
| 110 |
+
}
|
| 111 |
+
],
|
| 112 |
+
"outputs": [
|
| 113 |
+
{
|
| 114 |
+
"name": "CONDITIONING",
|
| 115 |
+
"type": "CONDITIONING",
|
| 116 |
+
"links": [
|
| 117 |
+
3,
|
| 118 |
+
4
|
| 119 |
+
],
|
| 120 |
+
"slot_index": 0
|
| 121 |
+
}
|
| 122 |
+
],
|
| 123 |
+
"properties": {
|
| 124 |
+
"Node name for S&R": "CLIPTextEncode"
|
| 125 |
+
},
|
| 126 |
+
"widgets_values": [
|
| 127 |
+
"A cluttered antique clockmaker's workshop seen through a cracked magnifying glass, brass gears laid out in a spiral on the workbench, a ginger cat asleep on a stack of leather-bound books, warm afternoon light with visible dust motes, photorealistic, 85mm lens, shallow depth of field"
|
| 128 |
+
],
|
| 129 |
+
"title": "Prompt",
|
| 130 |
+
"color": "#232",
|
| 131 |
+
"bgcolor": "#353"
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"id": 4,
|
| 135 |
+
"type": "ConditioningZeroOut",
|
| 136 |
+
"pos": [
|
| 137 |
+
900,
|
| 138 |
+
340.0
|
| 139 |
+
],
|
| 140 |
+
"size": [
|
| 141 |
+
400,
|
| 142 |
+
60
|
| 143 |
+
],
|
| 144 |
+
"flags": {},
|
| 145 |
+
"order": 3,
|
| 146 |
+
"mode": 0,
|
| 147 |
+
"inputs": [
|
| 148 |
+
{
|
| 149 |
+
"name": "conditioning",
|
| 150 |
+
"type": "CONDITIONING",
|
| 151 |
+
"link": 3
|
| 152 |
+
}
|
| 153 |
+
],
|
| 154 |
+
"outputs": [
|
| 155 |
+
{
|
| 156 |
+
"name": "CONDITIONING",
|
| 157 |
+
"type": "CONDITIONING",
|
| 158 |
+
"links": [
|
| 159 |
+
5
|
| 160 |
+
],
|
| 161 |
+
"slot_index": 0
|
| 162 |
+
}
|
| 163 |
+
],
|
| 164 |
+
"properties": {
|
| 165 |
+
"Node name for S&R": "ConditioningZeroOut"
|
| 166 |
+
},
|
| 167 |
+
"widgets_values": [],
|
| 168 |
+
"title": "Negative (zeroed - required at cfg 1.0)",
|
| 169 |
+
"color": "#232",
|
| 170 |
+
"bgcolor": "#353"
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"id": 5,
|
| 174 |
+
"type": "EmptySD3LatentImage",
|
| 175 |
+
"pos": [
|
| 176 |
+
900,
|
| 177 |
+
500.00000000000006
|
| 178 |
+
],
|
| 179 |
+
"size": [
|
| 180 |
+
400,
|
| 181 |
+
120
|
| 182 |
+
],
|
| 183 |
+
"flags": {},
|
| 184 |
+
"order": 4,
|
| 185 |
+
"mode": 0,
|
| 186 |
+
"inputs": [],
|
| 187 |
+
"outputs": [
|
| 188 |
+
{
|
| 189 |
+
"name": "LATENT",
|
| 190 |
+
"type": "LATENT",
|
| 191 |
+
"links": [
|
| 192 |
+
6
|
| 193 |
+
],
|
| 194 |
+
"slot_index": 0
|
| 195 |
+
}
|
| 196 |
+
],
|
| 197 |
+
"properties": {
|
| 198 |
+
"Node name for S&R": "EmptySD3LatentImage"
|
| 199 |
+
},
|
| 200 |
+
"widgets_values": [
|
| 201 |
+
1024,
|
| 202 |
+
1024,
|
| 203 |
+
1
|
| 204 |
+
],
|
| 205 |
+
"title": "Latent 1024x1024",
|
| 206 |
+
"color": "#232",
|
| 207 |
+
"bgcolor": "#353"
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"id": 6,
|
| 211 |
+
"type": "KSampler",
|
| 212 |
+
"pos": [
|
| 213 |
+
1320,
|
| 214 |
+
60
|
| 215 |
+
],
|
| 216 |
+
"size": [
|
| 217 |
+
400,
|
| 218 |
+
280
|
| 219 |
+
],
|
| 220 |
+
"flags": {},
|
| 221 |
+
"order": 5,
|
| 222 |
+
"mode": 0,
|
| 223 |
+
"inputs": [
|
| 224 |
+
{
|
| 225 |
+
"name": "model",
|
| 226 |
+
"type": "MODEL",
|
| 227 |
+
"link": 1
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"name": "positive",
|
| 231 |
+
"type": "CONDITIONING",
|
| 232 |
+
"link": 4
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
"name": "negative",
|
| 236 |
+
"type": "CONDITIONING",
|
| 237 |
+
"link": 5
|
| 238 |
+
},
|
| 239 |
+
{
|
| 240 |
+
"name": "latent_image",
|
| 241 |
+
"type": "LATENT",
|
| 242 |
+
"link": 6
|
| 243 |
+
}
|
| 244 |
+
],
|
| 245 |
+
"outputs": [
|
| 246 |
+
{
|
| 247 |
+
"name": "LATENT",
|
| 248 |
+
"type": "LATENT",
|
| 249 |
+
"links": [
|
| 250 |
+
7
|
| 251 |
+
],
|
| 252 |
+
"slot_index": 0
|
| 253 |
+
}
|
| 254 |
+
],
|
| 255 |
+
"properties": {
|
| 256 |
+
"Node name for S&R": "KSampler"
|
| 257 |
+
},
|
| 258 |
+
"widgets_values": [
|
| 259 |
+
987654321,
|
| 260 |
+
"randomize",
|
| 261 |
+
8,
|
| 262 |
+
1.0,
|
| 263 |
+
"euler",
|
| 264 |
+
"simple",
|
| 265 |
+
1.0
|
| 266 |
+
],
|
| 267 |
+
"title": "KSampler - 8 steps, cfg 1.0",
|
| 268 |
+
"color": "#323",
|
| 269 |
+
"bgcolor": "#535"
|
| 270 |
+
},
|
| 271 |
+
{
|
| 272 |
+
"id": 7,
|
| 273 |
+
"type": "VAEDecode",
|
| 274 |
+
"pos": [
|
| 275 |
+
1740,
|
| 276 |
+
60
|
| 277 |
+
],
|
| 278 |
+
"size": [
|
| 279 |
+
300,
|
| 280 |
+
60
|
| 281 |
+
],
|
| 282 |
+
"flags": {},
|
| 283 |
+
"order": 6,
|
| 284 |
+
"mode": 0,
|
| 285 |
+
"inputs": [
|
| 286 |
+
{
|
| 287 |
+
"name": "samples",
|
| 288 |
+
"type": "LATENT",
|
| 289 |
+
"link": 7
|
| 290 |
+
},
|
| 291 |
+
{
|
| 292 |
+
"name": "vae",
|
| 293 |
+
"type": "VAE",
|
| 294 |
+
"link": 8
|
| 295 |
+
}
|
| 296 |
+
],
|
| 297 |
+
"outputs": [
|
| 298 |
+
{
|
| 299 |
+
"name": "IMAGE",
|
| 300 |
+
"type": "IMAGE",
|
| 301 |
+
"links": [
|
| 302 |
+
9
|
| 303 |
+
],
|
| 304 |
+
"slot_index": 0
|
| 305 |
+
}
|
| 306 |
+
],
|
| 307 |
+
"properties": {
|
| 308 |
+
"Node name for S&R": "VAEDecode"
|
| 309 |
+
},
|
| 310 |
+
"widgets_values": [],
|
| 311 |
+
"title": "VAE Decode",
|
| 312 |
+
"color": "#323",
|
| 313 |
+
"bgcolor": "#535"
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
"id": 8,
|
| 317 |
+
"type": "SaveImage",
|
| 318 |
+
"pos": [
|
| 319 |
+
1740,
|
| 320 |
+
200.0
|
| 321 |
+
],
|
| 322 |
+
"size": [
|
| 323 |
+
400,
|
| 324 |
+
300
|
| 325 |
+
],
|
| 326 |
+
"flags": {},
|
| 327 |
+
"order": 7,
|
| 328 |
+
"mode": 0,
|
| 329 |
+
"inputs": [
|
| 330 |
+
{
|
| 331 |
+
"name": "images",
|
| 332 |
+
"type": "IMAGE",
|
| 333 |
+
"link": 9
|
| 334 |
+
}
|
| 335 |
+
],
|
| 336 |
+
"outputs": [],
|
| 337 |
+
"properties": {
|
| 338 |
+
"Node name for S&R": "SaveImage"
|
| 339 |
+
},
|
| 340 |
+
"widgets_values": [
|
| 341 |
+
"krea2_turbo_aio_svdq"
|
| 342 |
+
],
|
| 343 |
+
"title": "Save",
|
| 344 |
+
"color": "#323",
|
| 345 |
+
"bgcolor": "#535"
|
| 346 |
+
},
|
| 347 |
+
{
|
| 348 |
+
"id": 9,
|
| 349 |
+
"type": "Krea2SVDQuantDiagnostics",
|
| 350 |
+
"pos": [
|
| 351 |
+
1320,
|
| 352 |
+
540.0
|
| 353 |
+
],
|
| 354 |
+
"size": [
|
| 355 |
+
400,
|
| 356 |
+
130
|
| 357 |
+
],
|
| 358 |
+
"flags": {},
|
| 359 |
+
"order": 8,
|
| 360 |
+
"mode": 0,
|
| 361 |
+
"inputs": [
|
| 362 |
+
{
|
| 363 |
+
"name": "model",
|
| 364 |
+
"type": "MODEL",
|
| 365 |
+
"link": 10
|
| 366 |
+
}
|
| 367 |
+
],
|
| 368 |
+
"outputs": [
|
| 369 |
+
{
|
| 370 |
+
"name": "MODEL",
|
| 371 |
+
"type": "MODEL",
|
| 372 |
+
"links": [],
|
| 373 |
+
"slot_index": 0
|
| 374 |
+
},
|
| 375 |
+
{
|
| 376 |
+
"name": "STRING",
|
| 377 |
+
"type": "STRING",
|
| 378 |
+
"links": [],
|
| 379 |
+
"slot_index": 1
|
| 380 |
+
}
|
| 381 |
+
],
|
| 382 |
+
"properties": {
|
| 383 |
+
"Node name for S&R": "Krea2SVDQuantDiagnostics"
|
| 384 |
+
},
|
| 385 |
+
"widgets_values": [
|
| 386 |
+
"dispatch",
|
| 387 |
+
4096
|
| 388 |
+
],
|
| 389 |
+
"title": "Diagnostics (optional - run if slow)",
|
| 390 |
+
"color": "#432",
|
| 391 |
+
"bgcolor": "#653"
|
| 392 |
+
}
|
| 393 |
+
],
|
| 394 |
+
"links": [
|
| 395 |
+
[
|
| 396 |
+
1,
|
| 397 |
+
2,
|
| 398 |
+
0,
|
| 399 |
+
6,
|
| 400 |
+
0,
|
| 401 |
+
"MODEL"
|
| 402 |
+
],
|
| 403 |
+
[
|
| 404 |
+
2,
|
| 405 |
+
2,
|
| 406 |
+
1,
|
| 407 |
+
3,
|
| 408 |
+
0,
|
| 409 |
+
"CLIP"
|
| 410 |
+
],
|
| 411 |
+
[
|
| 412 |
+
3,
|
| 413 |
+
3,
|
| 414 |
+
0,
|
| 415 |
+
4,
|
| 416 |
+
0,
|
| 417 |
+
"CONDITIONING"
|
| 418 |
+
],
|
| 419 |
+
[
|
| 420 |
+
4,
|
| 421 |
+
3,
|
| 422 |
+
0,
|
| 423 |
+
6,
|
| 424 |
+
1,
|
| 425 |
+
"CONDITIONING"
|
| 426 |
+
],
|
| 427 |
+
[
|
| 428 |
+
5,
|
| 429 |
+
4,
|
| 430 |
+
0,
|
| 431 |
+
6,
|
| 432 |
+
2,
|
| 433 |
+
"CONDITIONING"
|
| 434 |
+
],
|
| 435 |
+
[
|
| 436 |
+
6,
|
| 437 |
+
5,
|
| 438 |
+
0,
|
| 439 |
+
6,
|
| 440 |
+
3,
|
| 441 |
+
"LATENT"
|
| 442 |
+
],
|
| 443 |
+
[
|
| 444 |
+
7,
|
| 445 |
+
6,
|
| 446 |
+
0,
|
| 447 |
+
7,
|
| 448 |
+
0,
|
| 449 |
+
"LATENT"
|
| 450 |
+
],
|
| 451 |
+
[
|
| 452 |
+
8,
|
| 453 |
+
2,
|
| 454 |
+
2,
|
| 455 |
+
7,
|
| 456 |
+
1,
|
| 457 |
+
"VAE"
|
| 458 |
+
],
|
| 459 |
+
[
|
| 460 |
+
9,
|
| 461 |
+
7,
|
| 462 |
+
0,
|
| 463 |
+
8,
|
| 464 |
+
0,
|
| 465 |
+
"IMAGE"
|
| 466 |
+
],
|
| 467 |
+
[
|
| 468 |
+
10,
|
| 469 |
+
2,
|
| 470 |
+
0,
|
| 471 |
+
9,
|
| 472 |
+
0,
|
| 473 |
+
"MODEL"
|
| 474 |
+
]
|
| 475 |
+
],
|
| 476 |
+
"groups": [
|
| 477 |
+
{
|
| 478 |
+
"title": "Load All-in-One",
|
| 479 |
+
"bounding": [
|
| 480 |
+
500,
|
| 481 |
+
20,
|
| 482 |
+
420,
|
| 483 |
+
280
|
| 484 |
+
],
|
| 485 |
+
"color": "#3f789e",
|
| 486 |
+
"font_size": 24,
|
| 487 |
+
"flags": {}
|
| 488 |
+
},
|
| 489 |
+
{
|
| 490 |
+
"title": "Prompt",
|
| 491 |
+
"bounding": [
|
| 492 |
+
940,
|
| 493 |
+
20,
|
| 494 |
+
420,
|
| 495 |
+
620
|
| 496 |
+
],
|
| 497 |
+
"color": "#3f789e",
|
| 498 |
+
"font_size": 24,
|
| 499 |
+
"flags": {}
|
| 500 |
+
},
|
| 501 |
+
{
|
| 502 |
+
"title": "Sample",
|
| 503 |
+
"bounding": [
|
| 504 |
+
1360,
|
| 505 |
+
20,
|
| 506 |
+
420,
|
| 507 |
+
640
|
| 508 |
+
],
|
| 509 |
+
"color": "#8a4",
|
| 510 |
+
"font_size": 24,
|
| 511 |
+
"flags": {}
|
| 512 |
+
}
|
| 513 |
+
],
|
| 514 |
+
"config": {},
|
| 515 |
+
"extra": {},
|
| 516 |
+
"version": 0.4
|
| 517 |
+
}
|
workflows/krea2_turbo_all_in_one_t2i_api.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"1": {
|
| 3 |
+
"class_type": "Krea2SVDQuantCheckpointLoader",
|
| 4 |
+
"inputs": {
|
| 5 |
+
"ckpt_name": "Krea2-Turbo-AllInOne-SVDQuant-W4A4-rank256-actaware-TEW4A4.safetensors"
|
| 6 |
+
},
|
| 7 |
+
"_meta": {
|
| 8 |
+
"title": "Krea2 SVDQuant Checkpoint Loader"
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"2": {
|
| 12 |
+
"class_type": "CLIPTextEncode",
|
| 13 |
+
"inputs": {
|
| 14 |
+
"text": "A cluttered antique clockmaker's workshop seen through a cracked magnifying glass, brass gears laid out in a spiral on the workbench, a ginger cat asleep on a stack of leather-bound books, warm afternoon light with visible dust motes, photorealistic, 85mm lens, shallow depth of field",
|
| 15 |
+
"clip": [
|
| 16 |
+
"1",
|
| 17 |
+
1
|
| 18 |
+
]
|
| 19 |
+
},
|
| 20 |
+
"_meta": {
|
| 21 |
+
"title": "Prompt"
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"3": {
|
| 25 |
+
"class_type": "ConditioningZeroOut",
|
| 26 |
+
"inputs": {
|
| 27 |
+
"conditioning": [
|
| 28 |
+
"2",
|
| 29 |
+
0
|
| 30 |
+
]
|
| 31 |
+
},
|
| 32 |
+
"_meta": {
|
| 33 |
+
"title": "Negative (zeroed - required at cfg 1.0)"
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"4": {
|
| 37 |
+
"class_type": "EmptySD3LatentImage",
|
| 38 |
+
"inputs": {
|
| 39 |
+
"width": 1024,
|
| 40 |
+
"height": 1024,
|
| 41 |
+
"batch_size": 1
|
| 42 |
+
},
|
| 43 |
+
"_meta": {
|
| 44 |
+
"title": "Latent 1024x1024"
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
"5": {
|
| 48 |
+
"class_type": "KSampler",
|
| 49 |
+
"inputs": {
|
| 50 |
+
"seed": 987654321,
|
| 51 |
+
"steps": 8,
|
| 52 |
+
"cfg": 1.0,
|
| 53 |
+
"sampler_name": "euler",
|
| 54 |
+
"scheduler": "simple",
|
| 55 |
+
"denoise": 1.0,
|
| 56 |
+
"model": [
|
| 57 |
+
"1",
|
| 58 |
+
0
|
| 59 |
+
],
|
| 60 |
+
"positive": [
|
| 61 |
+
"2",
|
| 62 |
+
0
|
| 63 |
+
],
|
| 64 |
+
"negative": [
|
| 65 |
+
"3",
|
| 66 |
+
0
|
| 67 |
+
],
|
| 68 |
+
"latent_image": [
|
| 69 |
+
"4",
|
| 70 |
+
0
|
| 71 |
+
]
|
| 72 |
+
},
|
| 73 |
+
"_meta": {
|
| 74 |
+
"title": "KSampler - 8 steps, cfg 1.0"
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"6": {
|
| 78 |
+
"class_type": "VAEDecode",
|
| 79 |
+
"inputs": {
|
| 80 |
+
"samples": [
|
| 81 |
+
"5",
|
| 82 |
+
0
|
| 83 |
+
],
|
| 84 |
+
"vae": [
|
| 85 |
+
"1",
|
| 86 |
+
2
|
| 87 |
+
]
|
| 88 |
+
},
|
| 89 |
+
"_meta": {
|
| 90 |
+
"title": "VAE Decode"
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
"7": {
|
| 94 |
+
"class_type": "SaveImage",
|
| 95 |
+
"inputs": {
|
| 96 |
+
"filename_prefix": "krea2_turbo_aio_svdq",
|
| 97 |
+
"images": [
|
| 98 |
+
"6",
|
| 99 |
+
0
|
| 100 |
+
]
|
| 101 |
+
},
|
| 102 |
+
"_meta": {
|
| 103 |
+
"title": "Save"
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
workflows/krea2_turbo_svdquant_w4a4_lora.json
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "00000000-0000-4000-8000-000000000000",
|
| 3 |
+
"revision": 0,
|
| 4 |
+
"last_node_id": 12,
|
| 5 |
+
"last_link_id": 11,
|
| 6 |
+
"nodes": [
|
| 7 |
+
{
|
| 8 |
+
"id": 1,
|
| 9 |
+
"type": "Note",
|
| 10 |
+
"pos": [
|
| 11 |
+
60,
|
| 12 |
+
60
|
| 13 |
+
],
|
| 14 |
+
"size": [
|
| 15 |
+
400,
|
| 16 |
+
720
|
| 17 |
+
],
|
| 18 |
+
"flags": {},
|
| 19 |
+
"order": 0,
|
| 20 |
+
"mode": 0,
|
| 21 |
+
"inputs": [],
|
| 22 |
+
"outputs": [],
|
| 23 |
+
"properties": {
|
| 24 |
+
"Node name for S&R": "Note"
|
| 25 |
+
},
|
| 26 |
+
"widgets_values": [
|
| 27 |
+
"KREA 2 TURBO + LoRA - SVDQuant W4A4\n\nSame graph as the plain Turbo one, with LoRA loaders inserted between\nthe checkpoint loader and the sampler.\n\n1. USE THIS NODE, NOT THE STOCK ONE. On a quantized checkpoint the\n stock LoraLoaderModelOnly has to dequantize the 4-bit weight, add\n the LoRA, and requantize it -- which throws away the format and\n quantizes the LoRA delta to 4 bits along with it. This node\n attaches the LoRA as a parallel branch instead:\n (W + BA)x == Wx + B(Ax), so the quantized weight is never touched.\n\n2. CHECK THE CONSOLE. The node prints what it matched, e.g.\n 224 quantized layers, 32 normal layers\n If it says 0 quantized layers, the LoRA does not target this model\n and nothing will change. That line is the first thing to look at\n when a LoRA \"does nothing\".\n\n3. STACKING. Chain more of these nodes, one per LoRA. The whole stack\n is refolded each time, so strengths stay exact no matter how many\n you chain, and the cost stays one pair of GEMMs. The second loader\n here is bypassed (ctrl-B) -- unmute it to use it.\n\n4. RANK MATTERS HERE. Without a LoRA, branch ranks 64/128/256 measure\n the same. With one, rank 256 wins clearly and rank 64 loses most of\n its advantage. If you use LoRAs, download rank 256.\n\n5. adapters INPUT. Only affects LoKr / LoHa / OFT, which cannot fold\n into the branch. \"bypass\" is exact and costs ~1.8s per model call\n at 1440x1920; \"bake\" is free per-step but requantizes the delta.\n A plain up/down LoRA ignores this setting entirely."
|
| 28 |
+
],
|
| 29 |
+
"title": "READ ME FIRST",
|
| 30 |
+
"color": "#432",
|
| 31 |
+
"bgcolor": "#653"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": 2,
|
| 35 |
+
"type": "Krea2SVDQuantW4A4Loader",
|
| 36 |
+
"pos": [
|
| 37 |
+
480,
|
| 38 |
+
60
|
| 39 |
+
],
|
| 40 |
+
"size": [
|
| 41 |
+
400,
|
| 42 |
+
120
|
| 43 |
+
],
|
| 44 |
+
"flags": {},
|
| 45 |
+
"order": 1,
|
| 46 |
+
"mode": 0,
|
| 47 |
+
"inputs": [],
|
| 48 |
+
"outputs": [
|
| 49 |
+
{
|
| 50 |
+
"name": "MODEL",
|
| 51 |
+
"type": "MODEL",
|
| 52 |
+
"links": [
|
| 53 |
+
1
|
| 54 |
+
],
|
| 55 |
+
"slot_index": 0
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"name": "STRING",
|
| 59 |
+
"type": "STRING",
|
| 60 |
+
"links": [],
|
| 61 |
+
"slot_index": 1
|
| 62 |
+
}
|
| 63 |
+
],
|
| 64 |
+
"properties": {
|
| 65 |
+
"Node name for S&R": "Krea2SVDQuantW4A4Loader"
|
| 66 |
+
},
|
| 67 |
+
"widgets_values": [
|
| 68 |
+
"Krea2-Turbo-SVDQuant-W4A4-rank256.safetensors"
|
| 69 |
+
],
|
| 70 |
+
"title": "Krea2 SVDQuant W4A4 Loader (rank 256 for LoRAs)",
|
| 71 |
+
"color": "#233",
|
| 72 |
+
"bgcolor": "#355"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"id": 3,
|
| 76 |
+
"type": "CLIPLoader",
|
| 77 |
+
"pos": [
|
| 78 |
+
480,
|
| 79 |
+
260
|
| 80 |
+
],
|
| 81 |
+
"size": [
|
| 82 |
+
400,
|
| 83 |
+
120
|
| 84 |
+
],
|
| 85 |
+
"flags": {},
|
| 86 |
+
"order": 2,
|
| 87 |
+
"mode": 0,
|
| 88 |
+
"inputs": [],
|
| 89 |
+
"outputs": [
|
| 90 |
+
{
|
| 91 |
+
"name": "CLIP",
|
| 92 |
+
"type": "CLIP",
|
| 93 |
+
"links": [
|
| 94 |
+
4
|
| 95 |
+
],
|
| 96 |
+
"slot_index": 0
|
| 97 |
+
}
|
| 98 |
+
],
|
| 99 |
+
"properties": {
|
| 100 |
+
"Node name for S&R": "CLIPLoader"
|
| 101 |
+
},
|
| 102 |
+
"widgets_values": [
|
| 103 |
+
"qwen3vl_4b_fp8_scaled.safetensors",
|
| 104 |
+
"krea2",
|
| 105 |
+
"default"
|
| 106 |
+
],
|
| 107 |
+
"title": "Text encoder (Qwen3-VL 4B)",
|
| 108 |
+
"color": "#233",
|
| 109 |
+
"bgcolor": "#355"
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"id": 4,
|
| 113 |
+
"type": "VAELoader",
|
| 114 |
+
"pos": [
|
| 115 |
+
480,
|
| 116 |
+
460
|
| 117 |
+
],
|
| 118 |
+
"size": [
|
| 119 |
+
400,
|
| 120 |
+
80
|
| 121 |
+
],
|
| 122 |
+
"flags": {},
|
| 123 |
+
"order": 3,
|
| 124 |
+
"mode": 0,
|
| 125 |
+
"inputs": [],
|
| 126 |
+
"outputs": [
|
| 127 |
+
{
|
| 128 |
+
"name": "VAE",
|
| 129 |
+
"type": "VAE",
|
| 130 |
+
"links": [
|
| 131 |
+
10
|
| 132 |
+
],
|
| 133 |
+
"slot_index": 0
|
| 134 |
+
}
|
| 135 |
+
],
|
| 136 |
+
"properties": {
|
| 137 |
+
"Node name for S&R": "VAELoader"
|
| 138 |
+
},
|
| 139 |
+
"widgets_values": [
|
| 140 |
+
"qwen_image_vae.safetensors"
|
| 141 |
+
],
|
| 142 |
+
"title": "VAE",
|
| 143 |
+
"color": "#233",
|
| 144 |
+
"bgcolor": "#355"
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"id": 5,
|
| 148 |
+
"type": "Krea2SVDQuantLoraLoader",
|
| 149 |
+
"pos": [
|
| 150 |
+
900,
|
| 151 |
+
60
|
| 152 |
+
],
|
| 153 |
+
"size": [
|
| 154 |
+
400,
|
| 155 |
+
140
|
| 156 |
+
],
|
| 157 |
+
"flags": {},
|
| 158 |
+
"order": 4,
|
| 159 |
+
"mode": 0,
|
| 160 |
+
"inputs": [
|
| 161 |
+
{
|
| 162 |
+
"name": "model",
|
| 163 |
+
"type": "MODEL",
|
| 164 |
+
"link": 1
|
| 165 |
+
}
|
| 166 |
+
],
|
| 167 |
+
"outputs": [
|
| 168 |
+
{
|
| 169 |
+
"name": "model",
|
| 170 |
+
"type": "MODEL",
|
| 171 |
+
"links": [
|
| 172 |
+
2,
|
| 173 |
+
3
|
| 174 |
+
],
|
| 175 |
+
"slot_index": 0
|
| 176 |
+
}
|
| 177 |
+
],
|
| 178 |
+
"properties": {
|
| 179 |
+
"Node name for S&R": "Krea2SVDQuantLoraLoader"
|
| 180 |
+
},
|
| 181 |
+
"widgets_values": [
|
| 182 |
+
"your_krea2_lora.safetensors",
|
| 183 |
+
1.0,
|
| 184 |
+
"bypass (exact, slower)"
|
| 185 |
+
],
|
| 186 |
+
"title": "Krea2 SVDQuant LoRA Loader",
|
| 187 |
+
"color": "#653",
|
| 188 |
+
"bgcolor": "#874"
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"id": 6,
|
| 192 |
+
"type": "Krea2SVDQuantLoraLoader",
|
| 193 |
+
"pos": [
|
| 194 |
+
900,
|
| 195 |
+
260
|
| 196 |
+
],
|
| 197 |
+
"size": [
|
| 198 |
+
400,
|
| 199 |
+
140
|
| 200 |
+
],
|
| 201 |
+
"flags": {},
|
| 202 |
+
"order": 5,
|
| 203 |
+
"mode": 4,
|
| 204 |
+
"inputs": [
|
| 205 |
+
{
|
| 206 |
+
"name": "model",
|
| 207 |
+
"type": "MODEL",
|
| 208 |
+
"link": 2
|
| 209 |
+
}
|
| 210 |
+
],
|
| 211 |
+
"outputs": [
|
| 212 |
+
{
|
| 213 |
+
"name": "model",
|
| 214 |
+
"type": "MODEL",
|
| 215 |
+
"links": [],
|
| 216 |
+
"slot_index": 0
|
| 217 |
+
}
|
| 218 |
+
],
|
| 219 |
+
"properties": {
|
| 220 |
+
"Node name for S&R": "Krea2SVDQuantLoraLoader"
|
| 221 |
+
},
|
| 222 |
+
"widgets_values": [
|
| 223 |
+
"your_krea2_lora.safetensors",
|
| 224 |
+
0.6,
|
| 225 |
+
"bypass (exact, slower)"
|
| 226 |
+
],
|
| 227 |
+
"title": "Second LoRA (muted - ctrl-B to enable)",
|
| 228 |
+
"color": "#653",
|
| 229 |
+
"bgcolor": "#874"
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"id": 7,
|
| 233 |
+
"type": "CLIPTextEncode",
|
| 234 |
+
"pos": [
|
| 235 |
+
1320,
|
| 236 |
+
60
|
| 237 |
+
],
|
| 238 |
+
"size": [
|
| 239 |
+
400,
|
| 240 |
+
220
|
| 241 |
+
],
|
| 242 |
+
"flags": {},
|
| 243 |
+
"order": 6,
|
| 244 |
+
"mode": 0,
|
| 245 |
+
"inputs": [
|
| 246 |
+
{
|
| 247 |
+
"name": "clip",
|
| 248 |
+
"type": "CLIP",
|
| 249 |
+
"link": 4
|
| 250 |
+
}
|
| 251 |
+
],
|
| 252 |
+
"outputs": [
|
| 253 |
+
{
|
| 254 |
+
"name": "CONDITIONING",
|
| 255 |
+
"type": "CONDITIONING",
|
| 256 |
+
"links": [
|
| 257 |
+
5,
|
| 258 |
+
6
|
| 259 |
+
],
|
| 260 |
+
"slot_index": 0
|
| 261 |
+
}
|
| 262 |
+
],
|
| 263 |
+
"properties": {
|
| 264 |
+
"Node name for S&R": "CLIPTextEncode"
|
| 265 |
+
},
|
| 266 |
+
"widgets_values": [
|
| 267 |
+
"A cluttered antique clockmaker's workshop seen through a cracked magnifying glass, brass gears laid out in a spiral on the workbench, a ginger cat asleep on a stack of leather-bound books, warm afternoon light with visible dust motes, photorealistic, 85mm lens, shallow depth of field"
|
| 268 |
+
],
|
| 269 |
+
"title": "Prompt (add your LoRA trigger words)",
|
| 270 |
+
"color": "#232",
|
| 271 |
+
"bgcolor": "#353"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"id": 8,
|
| 275 |
+
"type": "ConditioningZeroOut",
|
| 276 |
+
"pos": [
|
| 277 |
+
1320,
|
| 278 |
+
340.0
|
| 279 |
+
],
|
| 280 |
+
"size": [
|
| 281 |
+
400,
|
| 282 |
+
60
|
| 283 |
+
],
|
| 284 |
+
"flags": {},
|
| 285 |
+
"order": 7,
|
| 286 |
+
"mode": 0,
|
| 287 |
+
"inputs": [
|
| 288 |
+
{
|
| 289 |
+
"name": "conditioning",
|
| 290 |
+
"type": "CONDITIONING",
|
| 291 |
+
"link": 5
|
| 292 |
+
}
|
| 293 |
+
],
|
| 294 |
+
"outputs": [
|
| 295 |
+
{
|
| 296 |
+
"name": "CONDITIONING",
|
| 297 |
+
"type": "CONDITIONING",
|
| 298 |
+
"links": [
|
| 299 |
+
7
|
| 300 |
+
],
|
| 301 |
+
"slot_index": 0
|
| 302 |
+
}
|
| 303 |
+
],
|
| 304 |
+
"properties": {
|
| 305 |
+
"Node name for S&R": "ConditioningZeroOut"
|
| 306 |
+
},
|
| 307 |
+
"widgets_values": [],
|
| 308 |
+
"title": "Negative (zeroed - required at cfg 1.0)",
|
| 309 |
+
"color": "#232",
|
| 310 |
+
"bgcolor": "#353"
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
"id": 9,
|
| 314 |
+
"type": "EmptySD3LatentImage",
|
| 315 |
+
"pos": [
|
| 316 |
+
1320,
|
| 317 |
+
500.00000000000006
|
| 318 |
+
],
|
| 319 |
+
"size": [
|
| 320 |
+
400,
|
| 321 |
+
120
|
| 322 |
+
],
|
| 323 |
+
"flags": {},
|
| 324 |
+
"order": 8,
|
| 325 |
+
"mode": 0,
|
| 326 |
+
"inputs": [],
|
| 327 |
+
"outputs": [
|
| 328 |
+
{
|
| 329 |
+
"name": "LATENT",
|
| 330 |
+
"type": "LATENT",
|
| 331 |
+
"links": [
|
| 332 |
+
8
|
| 333 |
+
],
|
| 334 |
+
"slot_index": 0
|
| 335 |
+
}
|
| 336 |
+
],
|
| 337 |
+
"properties": {
|
| 338 |
+
"Node name for S&R": "EmptySD3LatentImage"
|
| 339 |
+
},
|
| 340 |
+
"widgets_values": [
|
| 341 |
+
1024,
|
| 342 |
+
1024,
|
| 343 |
+
1
|
| 344 |
+
],
|
| 345 |
+
"title": "Latent 1024x1024",
|
| 346 |
+
"color": "#232",
|
| 347 |
+
"bgcolor": "#353"
|
| 348 |
+
},
|
| 349 |
+
{
|
| 350 |
+
"id": 10,
|
| 351 |
+
"type": "KSampler",
|
| 352 |
+
"pos": [
|
| 353 |
+
1740,
|
| 354 |
+
60
|
| 355 |
+
],
|
| 356 |
+
"size": [
|
| 357 |
+
400,
|
| 358 |
+
280
|
| 359 |
+
],
|
| 360 |
+
"flags": {},
|
| 361 |
+
"order": 9,
|
| 362 |
+
"mode": 0,
|
| 363 |
+
"inputs": [
|
| 364 |
+
{
|
| 365 |
+
"name": "model",
|
| 366 |
+
"type": "MODEL",
|
| 367 |
+
"link": 3
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"name": "positive",
|
| 371 |
+
"type": "CONDITIONING",
|
| 372 |
+
"link": 6
|
| 373 |
+
},
|
| 374 |
+
{
|
| 375 |
+
"name": "negative",
|
| 376 |
+
"type": "CONDITIONING",
|
| 377 |
+
"link": 7
|
| 378 |
+
},
|
| 379 |
+
{
|
| 380 |
+
"name": "latent_image",
|
| 381 |
+
"type": "LATENT",
|
| 382 |
+
"link": 8
|
| 383 |
+
}
|
| 384 |
+
],
|
| 385 |
+
"outputs": [
|
| 386 |
+
{
|
| 387 |
+
"name": "LATENT",
|
| 388 |
+
"type": "LATENT",
|
| 389 |
+
"links": [
|
| 390 |
+
9
|
| 391 |
+
],
|
| 392 |
+
"slot_index": 0
|
| 393 |
+
}
|
| 394 |
+
],
|
| 395 |
+
"properties": {
|
| 396 |
+
"Node name for S&R": "KSampler"
|
| 397 |
+
},
|
| 398 |
+
"widgets_values": [
|
| 399 |
+
987654321,
|
| 400 |
+
"randomize",
|
| 401 |
+
8,
|
| 402 |
+
1.0,
|
| 403 |
+
"euler",
|
| 404 |
+
"simple",
|
| 405 |
+
1.0
|
| 406 |
+
],
|
| 407 |
+
"title": "KSampler - 8 steps, cfg 1.0",
|
| 408 |
+
"color": "#323",
|
| 409 |
+
"bgcolor": "#535"
|
| 410 |
+
},
|
| 411 |
+
{
|
| 412 |
+
"id": 11,
|
| 413 |
+
"type": "VAEDecode",
|
| 414 |
+
"pos": [
|
| 415 |
+
2160,
|
| 416 |
+
60
|
| 417 |
+
],
|
| 418 |
+
"size": [
|
| 419 |
+
300,
|
| 420 |
+
60
|
| 421 |
+
],
|
| 422 |
+
"flags": {},
|
| 423 |
+
"order": 10,
|
| 424 |
+
"mode": 0,
|
| 425 |
+
"inputs": [
|
| 426 |
+
{
|
| 427 |
+
"name": "samples",
|
| 428 |
+
"type": "LATENT",
|
| 429 |
+
"link": 9
|
| 430 |
+
},
|
| 431 |
+
{
|
| 432 |
+
"name": "vae",
|
| 433 |
+
"type": "VAE",
|
| 434 |
+
"link": 10
|
| 435 |
+
}
|
| 436 |
+
],
|
| 437 |
+
"outputs": [
|
| 438 |
+
{
|
| 439 |
+
"name": "IMAGE",
|
| 440 |
+
"type": "IMAGE",
|
| 441 |
+
"links": [
|
| 442 |
+
11
|
| 443 |
+
],
|
| 444 |
+
"slot_index": 0
|
| 445 |
+
}
|
| 446 |
+
],
|
| 447 |
+
"properties": {
|
| 448 |
+
"Node name for S&R": "VAEDecode"
|
| 449 |
+
},
|
| 450 |
+
"widgets_values": [],
|
| 451 |
+
"title": "VAE Decode",
|
| 452 |
+
"color": "#323",
|
| 453 |
+
"bgcolor": "#535"
|
| 454 |
+
},
|
| 455 |
+
{
|
| 456 |
+
"id": 12,
|
| 457 |
+
"type": "SaveImage",
|
| 458 |
+
"pos": [
|
| 459 |
+
2160,
|
| 460 |
+
200.0
|
| 461 |
+
],
|
| 462 |
+
"size": [
|
| 463 |
+
400,
|
| 464 |
+
300
|
| 465 |
+
],
|
| 466 |
+
"flags": {},
|
| 467 |
+
"order": 11,
|
| 468 |
+
"mode": 0,
|
| 469 |
+
"inputs": [
|
| 470 |
+
{
|
| 471 |
+
"name": "images",
|
| 472 |
+
"type": "IMAGE",
|
| 473 |
+
"link": 11
|
| 474 |
+
}
|
| 475 |
+
],
|
| 476 |
+
"outputs": [],
|
| 477 |
+
"properties": {
|
| 478 |
+
"Node name for S&R": "SaveImage"
|
| 479 |
+
},
|
| 480 |
+
"widgets_values": [
|
| 481 |
+
"krea2_turbo_lora"
|
| 482 |
+
],
|
| 483 |
+
"title": "Save",
|
| 484 |
+
"color": "#323",
|
| 485 |
+
"bgcolor": "#535"
|
| 486 |
+
}
|
| 487 |
+
],
|
| 488 |
+
"links": [
|
| 489 |
+
[
|
| 490 |
+
1,
|
| 491 |
+
2,
|
| 492 |
+
0,
|
| 493 |
+
5,
|
| 494 |
+
0,
|
| 495 |
+
"MODEL"
|
| 496 |
+
],
|
| 497 |
+
[
|
| 498 |
+
2,
|
| 499 |
+
5,
|
| 500 |
+
0,
|
| 501 |
+
6,
|
| 502 |
+
0,
|
| 503 |
+
"MODEL"
|
| 504 |
+
],
|
| 505 |
+
[
|
| 506 |
+
3,
|
| 507 |
+
5,
|
| 508 |
+
0,
|
| 509 |
+
10,
|
| 510 |
+
0,
|
| 511 |
+
"MODEL"
|
| 512 |
+
],
|
| 513 |
+
[
|
| 514 |
+
4,
|
| 515 |
+
3,
|
| 516 |
+
0,
|
| 517 |
+
7,
|
| 518 |
+
0,
|
| 519 |
+
"CLIP"
|
| 520 |
+
],
|
| 521 |
+
[
|
| 522 |
+
5,
|
| 523 |
+
7,
|
| 524 |
+
0,
|
| 525 |
+
8,
|
| 526 |
+
0,
|
| 527 |
+
"CONDITIONING"
|
| 528 |
+
],
|
| 529 |
+
[
|
| 530 |
+
6,
|
| 531 |
+
7,
|
| 532 |
+
0,
|
| 533 |
+
10,
|
| 534 |
+
1,
|
| 535 |
+
"CONDITIONING"
|
| 536 |
+
],
|
| 537 |
+
[
|
| 538 |
+
7,
|
| 539 |
+
8,
|
| 540 |
+
0,
|
| 541 |
+
10,
|
| 542 |
+
2,
|
| 543 |
+
"CONDITIONING"
|
| 544 |
+
],
|
| 545 |
+
[
|
| 546 |
+
8,
|
| 547 |
+
9,
|
| 548 |
+
0,
|
| 549 |
+
10,
|
| 550 |
+
3,
|
| 551 |
+
"LATENT"
|
| 552 |
+
],
|
| 553 |
+
[
|
| 554 |
+
9,
|
| 555 |
+
10,
|
| 556 |
+
0,
|
| 557 |
+
11,
|
| 558 |
+
0,
|
| 559 |
+
"LATENT"
|
| 560 |
+
],
|
| 561 |
+
[
|
| 562 |
+
10,
|
| 563 |
+
4,
|
| 564 |
+
0,
|
| 565 |
+
11,
|
| 566 |
+
1,
|
| 567 |
+
"VAE"
|
| 568 |
+
],
|
| 569 |
+
[
|
| 570 |
+
11,
|
| 571 |
+
11,
|
| 572 |
+
0,
|
| 573 |
+
12,
|
| 574 |
+
0,
|
| 575 |
+
"IMAGE"
|
| 576 |
+
]
|
| 577 |
+
],
|
| 578 |
+
"groups": [
|
| 579 |
+
{
|
| 580 |
+
"title": "Load",
|
| 581 |
+
"bounding": [
|
| 582 |
+
500,
|
| 583 |
+
20,
|
| 584 |
+
420,
|
| 585 |
+
620
|
| 586 |
+
],
|
| 587 |
+
"color": "#3f789e",
|
| 588 |
+
"font_size": 24,
|
| 589 |
+
"flags": {}
|
| 590 |
+
},
|
| 591 |
+
{
|
| 592 |
+
"title": "LoRA - chain one node per LoRA",
|
| 593 |
+
"bounding": [
|
| 594 |
+
940,
|
| 595 |
+
20,
|
| 596 |
+
420,
|
| 597 |
+
420
|
| 598 |
+
],
|
| 599 |
+
"color": "#a85",
|
| 600 |
+
"font_size": 24,
|
| 601 |
+
"flags": {}
|
| 602 |
+
},
|
| 603 |
+
{
|
| 604 |
+
"title": "Prompt",
|
| 605 |
+
"bounding": [
|
| 606 |
+
1360,
|
| 607 |
+
20,
|
| 608 |
+
420,
|
| 609 |
+
620
|
| 610 |
+
],
|
| 611 |
+
"color": "#3f789e",
|
| 612 |
+
"font_size": 24,
|
| 613 |
+
"flags": {}
|
| 614 |
+
},
|
| 615 |
+
{
|
| 616 |
+
"title": "Sample",
|
| 617 |
+
"bounding": [
|
| 618 |
+
1780,
|
| 619 |
+
20,
|
| 620 |
+
420,
|
| 621 |
+
640
|
| 622 |
+
],
|
| 623 |
+
"color": "#8a4",
|
| 624 |
+
"font_size": 24,
|
| 625 |
+
"flags": {}
|
| 626 |
+
}
|
| 627 |
+
],
|
| 628 |
+
"config": {},
|
| 629 |
+
"extra": {},
|
| 630 |
+
"version": 0.4
|
| 631 |
+
}
|
workflows/krea2_turbo_svdquant_w4a4_lora_api.json
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"1": {
|
| 3 |
+
"class_type": "Krea2SVDQuantW4A4Loader",
|
| 4 |
+
"inputs": {
|
| 5 |
+
"model_name": "Krea2-Turbo-SVDQuant-W4A4-rank256.safetensors"
|
| 6 |
+
},
|
| 7 |
+
"_meta": {
|
| 8 |
+
"title": "Krea2 SVDQuant W4A4 Loader (rank 256 for LoRAs)"
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"2": {
|
| 12 |
+
"class_type": "CLIPLoader",
|
| 13 |
+
"inputs": {
|
| 14 |
+
"clip_name": "qwen3vl_4b_fp8_scaled.safetensors",
|
| 15 |
+
"type": "krea2",
|
| 16 |
+
"device": "default"
|
| 17 |
+
},
|
| 18 |
+
"_meta": {
|
| 19 |
+
"title": "Text encoder (Qwen3-VL 4B)"
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"3": {
|
| 23 |
+
"class_type": "VAELoader",
|
| 24 |
+
"inputs": {
|
| 25 |
+
"vae_name": "qwen_image_vae.safetensors"
|
| 26 |
+
},
|
| 27 |
+
"_meta": {
|
| 28 |
+
"title": "VAE"
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
"4": {
|
| 32 |
+
"class_type": "Krea2SVDQuantLoraLoader",
|
| 33 |
+
"inputs": {
|
| 34 |
+
"lora_name": "your_krea2_lora.safetensors",
|
| 35 |
+
"strength": 1.0,
|
| 36 |
+
"adapters": "bypass (exact, slower)",
|
| 37 |
+
"model": [
|
| 38 |
+
"1",
|
| 39 |
+
0
|
| 40 |
+
]
|
| 41 |
+
},
|
| 42 |
+
"_meta": {
|
| 43 |
+
"title": "Krea2 SVDQuant LoRA Loader"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
"5": {
|
| 47 |
+
"class_type": "CLIPTextEncode",
|
| 48 |
+
"inputs": {
|
| 49 |
+
"text": "A cluttered antique clockmaker's workshop seen through a cracked magnifying glass, brass gears laid out in a spiral on the workbench, a ginger cat asleep on a stack of leather-bound books, warm afternoon light with visible dust motes, photorealistic, 85mm lens, shallow depth of field",
|
| 50 |
+
"clip": [
|
| 51 |
+
"2",
|
| 52 |
+
0
|
| 53 |
+
]
|
| 54 |
+
},
|
| 55 |
+
"_meta": {
|
| 56 |
+
"title": "Prompt (add your LoRA trigger words)"
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
"6": {
|
| 60 |
+
"class_type": "ConditioningZeroOut",
|
| 61 |
+
"inputs": {
|
| 62 |
+
"conditioning": [
|
| 63 |
+
"5",
|
| 64 |
+
0
|
| 65 |
+
]
|
| 66 |
+
},
|
| 67 |
+
"_meta": {
|
| 68 |
+
"title": "Negative (zeroed - required at cfg 1.0)"
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
"7": {
|
| 72 |
+
"class_type": "EmptySD3LatentImage",
|
| 73 |
+
"inputs": {
|
| 74 |
+
"width": 1024,
|
| 75 |
+
"height": 1024,
|
| 76 |
+
"batch_size": 1
|
| 77 |
+
},
|
| 78 |
+
"_meta": {
|
| 79 |
+
"title": "Latent 1024x1024"
|
| 80 |
+
}
|
| 81 |
+
},
|
| 82 |
+
"8": {
|
| 83 |
+
"class_type": "KSampler",
|
| 84 |
+
"inputs": {
|
| 85 |
+
"seed": 987654321,
|
| 86 |
+
"steps": 8,
|
| 87 |
+
"cfg": 1.0,
|
| 88 |
+
"sampler_name": "euler",
|
| 89 |
+
"scheduler": "simple",
|
| 90 |
+
"denoise": 1.0,
|
| 91 |
+
"model": [
|
| 92 |
+
"4",
|
| 93 |
+
0
|
| 94 |
+
],
|
| 95 |
+
"positive": [
|
| 96 |
+
"5",
|
| 97 |
+
0
|
| 98 |
+
],
|
| 99 |
+
"negative": [
|
| 100 |
+
"6",
|
| 101 |
+
0
|
| 102 |
+
],
|
| 103 |
+
"latent_image": [
|
| 104 |
+
"7",
|
| 105 |
+
0
|
| 106 |
+
]
|
| 107 |
+
},
|
| 108 |
+
"_meta": {
|
| 109 |
+
"title": "KSampler - 8 steps, cfg 1.0"
|
| 110 |
+
}
|
| 111 |
+
},
|
| 112 |
+
"9": {
|
| 113 |
+
"class_type": "VAEDecode",
|
| 114 |
+
"inputs": {
|
| 115 |
+
"samples": [
|
| 116 |
+
"8",
|
| 117 |
+
0
|
| 118 |
+
],
|
| 119 |
+
"vae": [
|
| 120 |
+
"3",
|
| 121 |
+
0
|
| 122 |
+
]
|
| 123 |
+
},
|
| 124 |
+
"_meta": {
|
| 125 |
+
"title": "VAE Decode"
|
| 126 |
+
}
|
| 127 |
+
},
|
| 128 |
+
"10": {
|
| 129 |
+
"class_type": "SaveImage",
|
| 130 |
+
"inputs": {
|
| 131 |
+
"filename_prefix": "krea2_turbo_lora",
|
| 132 |
+
"images": [
|
| 133 |
+
"9",
|
| 134 |
+
0
|
| 135 |
+
]
|
| 136 |
+
},
|
| 137 |
+
"_meta": {
|
| 138 |
+
"title": "Save"
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
}
|