Text-to-Image
Diffusers
image-generation
comfyui
quantization
int8
int4
svdquant
krea2
krea
diffusion
transformer
lowvram
Instructions to use AlperKTS/Krea-2-SVDQuant-ComfyUI with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use AlperKTS/Krea-2-SVDQuant-ComfyUI with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("AlperKTS/Krea-2-SVDQuant-ComfyUI", torch_dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
- DiffusionBee
File size: 17,633 Bytes
032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee d50078f 032e1ee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 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 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 | """Generate the UI-format workflow JSONs under ``workflows/``.
Run this, do not hand-edit the output:
python tools/build_workflows.py
ComfyUI has two JSON dialects and they are not interchangeable.
* **API format** -- ``{"1": {"class_type": ..., "inputs": {...}}, ...}``. What you POST to
``/prompt``. Carries no layout, no titles, no notes, no colours.
* **UI format** -- ``{"nodes": [...], "links": [...], ...}``. What the editor saves and what
drag-and-drop expects.
The repo used to ship only API format while the README told people to drag the file in, so
every new user's first experience was a pile of untitled nodes stacked on the origin. Both
dialects come out of this script now -- ``*_api.json`` for scripting, the plain name for the
editor -- from one graph definition, so they cannot drift apart. They previously could, and
did: the committed API files carried titles the generator had never produced.
Positions are computed from a column/row grid rather than written by hand, so adding a node
does not mean renumbering everything below it.
"""
from __future__ import annotations
import json
import os
HERE = os.path.dirname(os.path.abspath(__file__))
OUT_DIR = os.path.join(os.path.dirname(HERE), "workflows")
# Widget order matters: ComfyUI matches `widgets_values` positionally against the node's
# required inputs, so these lists follow each node class's INPUT_TYPES order exactly.
COL = 420 # horizontal gap between columns
ROW = 200 # vertical gap between rows
TEAL = ("#233", "#355") # loaders
GREEN = ("#232", "#353") # prompts
PURPLE = ("#323", "#535") # sampling
YELLOW = ("#432", "#653") # notes / read me first
class Graph:
"""Minimal UI-format graph builder.
Only what these workflows need: typed slots, links, groups, colours. Not a general
LiteGraph serializer.
"""
def __init__(self):
self.nodes = []
self.links = []
self._next_node = 1
self._next_link = 1
self._slots = {} # node_id -> {"in": {name: idx}, "out": {name: idx}}
self._api = {} # node_id -> what the API dialect needs about that node
def add(self, class_type, col, row, inputs=None, outputs=None, widgets=None,
title=None, colour=None, size=None, api=True):
"""`widgets` is a list of (input_name, value).
The name is what the API dialect needs and the UI dialect ignores; `None` marks a
UI-only widget (`control_after_generate` has no API input). Keeping them as pairs is
what lets one graph definition emit both dialects, instead of a hand-written API file
drifting away from the generator -- which is exactly what had happened.
`api=False` drops the node from the API output: Notes and the optional Diagnostics
node are editor furniture with nothing to execute.
"""
nid = self._next_node
self._next_node += 1
inputs = inputs or []
outputs = outputs or []
widgets = widgets or []
self._slots[nid] = {
"in": {name: i for i, (name, _) in enumerate(inputs)},
"out": {name: i for i, (name, _) in enumerate(outputs)},
}
node = {
"id": nid,
"type": class_type,
"pos": [60 + col * COL, 60 + row * ROW],
"size": size or [380, 100],
"flags": {},
"order": self._next_node - 2,
"mode": 0,
"inputs": [{"name": n, "type": t, "link": None} for n, t in inputs],
"outputs": [{"name": n, "type": t, "links": [], "slot_index": i}
for i, (n, t) in enumerate(outputs)],
"properties": {"Node name for S&R": class_type},
"widgets_values": [v for _name, v in widgets],
}
if title:
node["title"] = title
if colour:
node["color"], node["bgcolor"] = colour
self.nodes.append(node)
self._api[nid] = {"class_type": class_type, "title": title, "api": api,
"widgets": [(n, v) for n, v in widgets if n is not None]}
return nid
def link(self, src_node, src_name, dst_node, dst_name):
src_slot = self._slots[src_node]["out"][src_name]
dst_slot = self._slots[dst_node]["in"][dst_name]
link_type = next(n for n in self.nodes if n["id"] == src_node)["outputs"][src_slot]["type"]
lid = self._next_link
self._next_link += 1
self.links.append([lid, src_node, src_slot, dst_node, dst_slot, link_type])
for n in self.nodes:
if n["id"] == src_node:
n["outputs"][src_slot]["links"].append(lid)
elif n["id"] == dst_node:
n["inputs"][dst_slot]["link"] = lid
return lid
def note(self, text, col, row, size=(400, 260), title="READ ME"):
"""A built-in Note node. Its text lives in widgets_values[0]."""
return self.add("Note", col, row, widgets=[(None, text)], title=title, colour=YELLOW,
size=list(size), api=False)
def group(self, title, bounding, colour="#3f789e"):
return {"title": title, "bounding": list(bounding), "color": colour,
"font_size": 24, "flags": {}}
def serialize(self, groups=()):
return {
"id": "00000000-0000-4000-8000-000000000000",
"revision": 0,
"last_node_id": self._next_node - 1,
"last_link_id": self._next_link - 1,
"nodes": self.nodes,
"links": self.links,
"groups": list(groups),
"config": {},
"extra": {},
"version": 0.4,
}
def serialize_api(self):
"""The same graph in API dialect: {"1": {"class_type", "inputs", "_meta"}, ...}.
Ids are reassigned sequentially over the executable nodes, so dropping the Note (which
is always added first) leaves the remaining nodes numbered from 1 with no gaps.
"""
keep = [n["id"] for n in self.nodes if self._api[n["id"]]["api"]]
renum = {old: str(i + 1) for i, old in enumerate(keep)}
# dst_node -> {input_name: [src_id, src_slot]}
wired: dict[int, dict] = {}
for _lid, src, src_slot, dst, dst_slot, _type in self.links:
if src not in renum or dst not in renum:
continue
name = next(n for n, i in self._slots[dst]["in"].items() if i == dst_slot)
wired.setdefault(dst, {})[name] = [renum[src], src_slot]
out = {}
for old in keep:
meta = self._api[old]
inputs = {name: value for name, value in meta["widgets"]}
inputs.update(wired.get(old, {}))
entry = {"class_type": meta["class_type"], "inputs": inputs}
if meta["title"]:
entry["_meta"] = {"title": meta["title"]}
out[renum[old]] = entry
return out
TURBO_NOTE = """KREA 2 TURBO - SVDQuant W4A4
1. Checkpoint goes in ComfyUI/models/diffusion_models/.
This graph expects an svdq checkpoint (it carries *.svdq_l1 /
*.svdq_l2 tensors). The --format w4a4 / int8 / fp8 files have no
low-rank branch and load with the stock UNETLoader instead.
2. cfg MUST be 1.0. Krea 2 Turbo is cfg-distilled, so the negative
prompt is zeroed out (ConditioningZeroOut) rather than encoded.
Raising cfg here degrades the image, it does not sharpen it.
8 steps is what the checkpoint was distilled for.
3. Text encoder: any Qwen3-VL 4B in ComfyUI/models/text_encoders/,
loaded with CLIPLoader type "krea2".
VAE: qwen_image_vae.safetensors.
4. SLOW? Read the loader's "status" output. It names the kernel that
will actually run. If it does not say cuda, ComfyUI has disabled
comfy_kitchen's CUDA backend because torch was built against
CUDA < 13 -- and the fallback dequantizes int4 in Python, so the
checkpoint ends up slower than fp8. Install a cu130+ torch build.
The Krea2 SVDQuant Env Check node answers this with no model loaded.
5. LoRA: use the Krea2 SVDQuant LoRA Loader, not the stock one. The
stock loader cannot patch a quantized weight and skips the blocks."""
BASE_NOTE = """KREA 2 BASE (non-turbo) - SVDQuant W4A4
The loader node will be outlined in RED when you open this. That is
expected: the filename it wants does not exist until you build it in
step 1, and ComfyUI flags any dropdown value it cannot find. Build the
checkpoint, then reselect it in the loader.
1. There is no pre-built base checkpoint to download. Make one:
python quantize_krea2.py raw.safetensors \\
--format svdq --rank 64 --variant base
That writes Krea2-Base-SVDQuant-W4A4-rank64.safetensors next to
the source. ~54s without refinement, ~5.7min with it.
The Krea2 SVDQuant Quantize node does the same thing from inside
ComfyUI if you would rather not touch a terminal.
2. Unlike Turbo, this model is NOT cfg-distilled, so it uses real
classifier-free guidance: cfg 3.5 and a real negative prompt
(a second CLIPTextEncode, not ConditioningZeroOut).
~50 steps. These are starting points, tune them.
3. This is roughly 12x the sampling work of the 8-step Turbo graph.
Every benchmark number in the README is Turbo at 8 steps - the
ratios carry over, the absolute seconds do not.
4. Text encoder and VAE are the same as the Turbo graph.
Same slow-generation checklist too: read the loader's status
output first."""
PROMPT = ("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")
NEGATIVE = "blurry, low resolution, jpeg artifacts, watermark, deformed, extra limbs"
def build_turbo():
g = Graph()
g.note(TURBO_NOTE, 0, 0, size=(400, 620), title="READ ME FIRST")
loader = g.add("Krea2SVDQuantW4A4Loader", 1, 0,
outputs=[("MODEL", "MODEL"), ("STRING", "STRING")],
widgets=[("model_name", "Krea2-Turbo-SVDQuant-W4A4-rank64.safetensors")],
title="Krea2 SVDQuant W4A4 Loader", colour=TEAL, size=[400, 120])
clip = g.add("CLIPLoader", 1, 1,
outputs=[("CLIP", "CLIP")],
widgets=[("clip_name", "qwen3vl_4b_fp8_scaled.safetensors"), ("type", "krea2"),
("device", "default")],
title="Text encoder (Qwen3-VL 4B)", colour=TEAL, size=[400, 120])
vae = g.add("VAELoader", 1, 2, outputs=[("VAE", "VAE")],
widgets=[("vae_name", "qwen_image_vae.safetensors")], title="VAE", colour=TEAL,
size=[400, 80])
pos = g.add("CLIPTextEncode", 2, 0, inputs=[("clip", "CLIP")],
outputs=[("CONDITIONING", "CONDITIONING")], widgets=[("text", PROMPT)],
title="Prompt", colour=GREEN, size=[400, 220])
neg = g.add("ConditioningZeroOut", 2, 1.4, inputs=[("conditioning", "CONDITIONING")],
outputs=[("CONDITIONING", "CONDITIONING")],
title="Negative (zeroed - required at cfg 1.0)", colour=GREEN,
size=[400, 60])
latent = g.add("EmptySD3LatentImage", 2, 2.2, outputs=[("LATENT", "LATENT")],
widgets=[("width", 1024), ("height", 1024), ("batch_size", 1)],
title="Latent 1024x1024", colour=GREEN,
size=[400, 120])
sampler = g.add("KSampler", 3, 0,
inputs=[("model", "MODEL"), ("positive", "CONDITIONING"),
("negative", "CONDITIONING"), ("latent_image", "LATENT")],
outputs=[("LATENT", "LATENT")],
widgets=[("seed", 987654321), (None, "randomize"), ("steps", 8), ("cfg", 1.0),
("sampler_name", "euler"), ("scheduler", "simple"),
("denoise", 1.0)],
title="KSampler - 8 steps, cfg 1.0", colour=PURPLE, size=[400, 280])
decode = g.add("VAEDecode", 4, 0, inputs=[("samples", "LATENT"), ("vae", "VAE")],
outputs=[("IMAGE", "IMAGE")], title="VAE Decode", colour=PURPLE,
size=[300, 60])
save = g.add("SaveImage", 4, 0.7, inputs=[("images", "IMAGE")],
widgets=[("filename_prefix", "krea2_turbo_svdq")], title="Save", colour=PURPLE, size=[400, 300])
diag = g.add("Krea2SVDQuantDiagnostics", 3, 2.4, inputs=[("model", "MODEL")],
outputs=[("MODEL", "MODEL"), ("STRING", "STRING")],
widgets=[("mode", "dispatch"), ("tokens", 4096)],
title="Diagnostics (optional - run if slow)", colour=YELLOW,
size=[400, 130], api=False)
g.link(loader, "MODEL", sampler, "model")
g.link(clip, "CLIP", pos, "clip")
g.link(pos, "CONDITIONING", neg, "conditioning")
g.link(pos, "CONDITIONING", sampler, "positive")
g.link(neg, "CONDITIONING", sampler, "negative")
g.link(latent, "LATENT", sampler, "latent_image")
g.link(sampler, "LATENT", decode, "samples")
g.link(vae, "VAE", decode, "vae")
g.link(decode, "IMAGE", save, "images")
g.link(loader, "MODEL", diag, "model")
groups = [
g.group("Load", (500, 20, 420, 620)),
g.group("Prompt", (940, 20, 420, 620)),
g.group("Sample", (1360, 20, 420, 640), colour="#8a4"),
]
return g.serialize(groups), g.serialize_api()
def build_base():
g = Graph()
g.note(BASE_NOTE, 0, 0, size=(400, 560), title="READ ME FIRST")
loader = g.add("Krea2SVDQuantW4A4Loader", 1, 0,
outputs=[("MODEL", "MODEL"), ("STRING", "STRING")],
widgets=[("model_name", "Krea2-Base-SVDQuant-W4A4-rank64.safetensors")],
title="Krea2 SVDQuant W4A4 Loader (base)", colour=TEAL, size=[400, 120])
clip = g.add("CLIPLoader", 1, 1, outputs=[("CLIP", "CLIP")],
widgets=[("clip_name", "qwen3vl_4b_fp8_scaled.safetensors"), ("type", "krea2"),
("device", "default")],
title="Text encoder (Qwen3-VL 4B)", colour=TEAL, size=[400, 120])
vae = g.add("VAELoader", 1, 2, outputs=[("VAE", "VAE")],
widgets=[("vae_name", "qwen_image_vae.safetensors")], title="VAE", colour=TEAL,
size=[400, 80])
pos = g.add("CLIPTextEncode", 2, 0, inputs=[("clip", "CLIP")],
outputs=[("CONDITIONING", "CONDITIONING")], widgets=[("text", PROMPT)],
title="Positive prompt", colour=GREEN, size=[400, 200])
neg = g.add("CLIPTextEncode", 2, 1.3, inputs=[("clip", "CLIP")],
outputs=[("CONDITIONING", "CONDITIONING")], widgets=[("text", NEGATIVE)],
title="Negative prompt (base uses real CFG)", colour=GREEN,
size=[400, 140])
latent = g.add("EmptySD3LatentImage", 2, 2.3, outputs=[("LATENT", "LATENT")],
widgets=[("width", 1024), ("height", 1024), ("batch_size", 1)],
title="Latent 1024x1024", colour=GREEN,
size=[400, 120])
sampler = g.add("KSampler", 3, 0,
inputs=[("model", "MODEL"), ("positive", "CONDITIONING"),
("negative", "CONDITIONING"), ("latent_image", "LATENT")],
outputs=[("LATENT", "LATENT")],
widgets=[("seed", 987654321), (None, "randomize"), ("steps", 50), ("cfg", 3.5),
("sampler_name", "euler"), ("scheduler", "simple"),
("denoise", 1.0)],
title="KSampler - 50 steps, cfg 3.5", colour=PURPLE, size=[400, 280])
decode = g.add("VAEDecode", 4, 0, inputs=[("samples", "LATENT"), ("vae", "VAE")],
outputs=[("IMAGE", "IMAGE")], title="VAE Decode", colour=PURPLE,
size=[300, 60])
save = g.add("SaveImage", 4, 0.7, inputs=[("images", "IMAGE")],
widgets=[("filename_prefix", "krea2_base_svdq")], title="Save", colour=PURPLE, size=[400, 300])
g.link(loader, "MODEL", sampler, "model")
g.link(clip, "CLIP", pos, "clip")
g.link(clip, "CLIP", neg, "clip")
g.link(pos, "CONDITIONING", sampler, "positive")
g.link(neg, "CONDITIONING", sampler, "negative")
g.link(latent, "LATENT", sampler, "latent_image")
g.link(sampler, "LATENT", decode, "samples")
g.link(vae, "VAE", decode, "vae")
g.link(decode, "IMAGE", save, "images")
groups = [
g.group("Load", (500, 20, 420, 560)),
g.group("Prompt", (940, 20, 420, 620)),
g.group("Sample", (1360, 20, 420, 640), colour="#8a4"),
]
return g.serialize(groups), g.serialize_api()
def _write(path, payload):
with open(path, "w", encoding="utf-8", newline="\n") as fh:
json.dump(payload, fh, indent=2, ensure_ascii=False)
fh.write("\n")
def main():
os.makedirs(OUT_DIR, exist_ok=True)
for name, build in (("krea2_turbo_svdquant_w4a4_t2i", build_turbo),
("krea2_base_svdquant_w4a4_t2i", build_base)):
graph, api = build()
ui_path = os.path.join(OUT_DIR, name + ".json")
api_path = os.path.join(OUT_DIR, name + "_api.json")
_write(ui_path, graph)
_write(api_path, api)
print("wrote {} ({} nodes, {} links)".format(
ui_path, len(graph["nodes"]), len(graph["links"])))
print("wrote {} ({} nodes)".format(api_path, len(api)))
if __name__ == "__main__":
main()
|