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", 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: 25,835 Bytes
d50078f b0956bc d50078f 1ace05b d50078f b0956bc d50078f b0956bc d50078f b0956bc d50078f b0956bc d50078f b0956bc d50078f b0956bc d50078f b0956bc d50078f b0956bc d50078f 1ace05b d50078f b0956bc d50078f b0956bc d50078f | 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 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 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 | """Loader for SVDQuant-on-native-W4A4 checkpoints (built by ``quantize_krea2.py``).
This is SVDQuant's mechanism (low-rank bf16 branch + 4-bit residual) running on ComfyUI's
own ``convrot_w4a4`` kernel. Because the *activations* are 4-bit here too, the matmul runs
on hardware that is genuinely faster than bf16 -- weight-only 4-bit schemes keep 16-bit
activations and therefore still run at bf16 tensor-core speed.
The checkpoint is self-contained - it carries the quantized blocks *and* the untouched
high-precision layers - so no separate base file is needed. Everything except the 224
block linears loads through ComfyUI's normal path; only those 224 get a low-rank branch
attached on top of the native quantized Linear.
"""
from __future__ import annotations
import logging
import os
import torch
import torch.nn.functional as F
import comfy.model_management
import comfy.sd
import comfy.utils
import folder_paths
from .quantize_krea2 import detect_prefix
from .sage_mask_guard import install_mask_guard
from .svdquant_diag import BUF_L1, BUF_L2, _CATEGORY, branch_factors, log_dispatch # noqa: F401
# The checkpoint keys are the buffer names with a dot in front -- derived rather than
# retyped, because the two being identical is the property the round-trip depends on.
_L1 = "." + BUF_L1
_L2 = "." + BUF_L2
def has_branch(module: torch.nn.Module) -> bool:
"""True once this module carries a low-rank branch (i.e. it is a quantized linear)."""
return BUF_L1 in getattr(module, "_buffers", {})
def add_low_rank(y: torch.Tensor, x: torch.Tensor, l1: torch.Tensor, l2: torch.Tensor):
"""``y + (x @ l2.T) @ l1.T``, with l1/l2 moved to x's device and dtype as needed.
`comfy.model_management.cast_to` returns the tensor untouched when it is already in
the right place, so this is free on a fully-resident model. When ComfyUI has offloaded
the layer, it performs the same kind of per-call staging copy that ComfyUI itself does
for the quantized weight -- crucially *without* caching the result back onto the
module, which would strand the factors on the GPU behind ComfyUI's back.
"""
a1 = comfy.model_management.cast_to(l1, x.dtype, x.device)
a2 = comfy.model_management.cast_to(l2, x.dtype, x.device)
return y + F.linear(F.linear(x, a2), a1)
def _publish_in_state_dict(module: torch.nn.Module) -> None:
"""Make the branch buffers visible to ``state_dict()`` on a quantized Linear.
Registering them as persistent buffers is not enough. ComfyUI's quantized Linear
(``comfy.ops.mixed_precision_ops``) replaces ``state_dict`` wholesale with a body that
emits weight/scale/marker/bias and nothing else -- buffers never reach it. That
matters because ``model_management.module_size()`` sums ``state_dict()``, and every
per-module VRAM decision ComfyUI makes (the lowvram split in particular) is derived
from that number. Left alone, ~2.9 MB per layer and ~645 MB across the 224 blocks
would sit on the GPU while the budget believed it was free -- which is exactly how an
8 GB card OOMs on this checkpoint but not on the branch-free int8 one.
Emitting them under their own names also means a model saved out of ComfyUI carries
the same ``<layer>.svdq_l1`` keys quantize_krea2.py writes, so it round-trips.
"""
if getattr(module, "_krea2_state_dict_patched", False):
return
inner = module.state_dict
def state_dict(*args, destination=None, prefix="", **kwargs):
sd = inner(*args, destination=destination, prefix=prefix, **kwargs)
for name in (BUF_L1, BUF_L2):
buf = module._buffers.get(name)
if buf is not None:
sd["{}{}".format(prefix, name)] = buf
return sd
module.state_dict = state_dict
module._krea2_state_dict_patched = True
def attach_branch(module: torch.nn.Module, l1: torch.Tensor, l2: torch.Tensor,
scale: float = 1.0) -> None:
"""Add ``+ (x @ l2.T) @ l1.T * scale`` to a module's output, in place.
The module is *not* replaced: swapping it for a wrapper would push its weight down a
level in the state dict (``blocks.0.attn.wq.base.weight``), and every LoRA key map in
ComfyUI expects ``blocks.0.attn.wq.weight``. Keeping the module identity keeps those
paths - and therefore the rest of the ecosystem - intact.
The factors are registered as persistent buffers *and* published into ``state_dict()``
by `_publish_in_state_dict` -- on these modules persistence alone is not enough, see
that function for why. Both steps are needed to get them counted by
``model_management.module_size()``, which is what every VRAM decision (full load, the
lowvram split, how much to free) is derived from. Uncounted, they are roughly 2.9 MB
per layer and 645 MB across the 224 blocks: the difference between fitting and OOMing
on an 8 GB card.
`scale` is folded into l2 at attach time rather than applied to the output every step:
l2 is [rank, in_features] while the output is [tokens, out_features], so this is about
three orders of magnitude less work and one fewer full-size allocation per call.
"""
if has_branch(module):
raise RuntimeError("module already carries a low-rank branch")
if scale != 1.0:
l2 = l2 * scale
module.register_buffer(BUF_L1, l1.contiguous(), persistent=True)
module.register_buffer(BUF_L2, l2.contiguous(), persistent=True)
_publish_in_state_dict(module)
original = module.forward
def forward(x, *args, **kwargs):
y = original(x, *args, **kwargs)
factors = branch_factors(module)
if factors is None:
return y
return add_low_rank(y, x, *factors)
module.forward = forward
# Kept as a stable handle so a LoRA can build on top of "quantized weight + svdq
# branch" without having to trust whatever is currently in `module.forward` -- which
# ComfyUI's object-patch machinery swaps in and out around sampling.
module._krea2_forward = forward
def _get_submodule(root: torch.nn.Module, dotted: str) -> torch.nn.Module:
"""Walk a dotted path, reporting the layer name rather than the raw attribute error.
Left to `getattr`/`__getitem__` this surfaces as a bare ``AttributeError: 'ModuleList'
object has no attribute 'nope'`` or ``IndexError: index 999 is out of range``, which says
nothing about which checkpoint key failed to map.
"""
module = root
for i, p in enumerate(dotted.split(".")):
try:
module = module[int(p)] if p.isdigit() else getattr(module, p)
except (AttributeError, IndexError, KeyError) as exc:
raise RuntimeError(
"checkpoint refers to layer {!r}, which this model does not have "
"(failed at {!r}): {}".format(dotted, ".".join(dotted.split(".")[:i + 1]), exc)
) from exc
return module
def _shield_from_dynamo(module: torch.nn.Module) -> None:
"""Fallback path: let torch.compile skip the quantized kernel instead of failing on it.
Dynamo cannot trace ``F.linear(x, QuantizedTensor)`` -- comfy_kitchen dispatches that
through ``__torch_dispatch__`` into a C extension that wants real pointers, so
fake-tensor tracing raises. Marking the call as a graph break lets inductor still fuse
everything *around* it (norms, modulation, RoPE), which is a third of the step time.
Measured cost of doing it this way: **two graph breaks per quantized layer**, 448 across
the 224 blocks (`diagnose.py --mode compile`). Inductor never sees two consecutive
layers in one graph and cudagraphs is off entirely. `_install_custom_op` is the path
that avoids this; this one remains for when the kitchen layout it depends on has moved.
Call order matters and is easy to get backwards: this must run *before*
`attach_branch`, so that what gets wrapped is the real `nn.Linear.forward` and the
branch closure installed on top of it stays traceable.
"""
try:
module.forward = torch._dynamo.disable(module.forward)
except Exception as exc:
# Not fatal on its own -- the model runs fine uncompiled. But if this silently
# no-ops, a later TorchCompileModel dies inside the comfy_kitchen kernel with a
# fake-tensor error that points nowhere near here, so leave a trail.
logging.debug("[krea2-svdquant] could not shield %s from dynamo: %s",
type(module).__name__, exc)
# The opaque op. Registering the kernel call under `torch.library` is the whole trick:
# Dynamo does not try to trace *into* a custom op, it emits a single node for it, so the
# 224 linears stop being graph breaks and a compiled step becomes one graph. The kitchen
# call inside is byte-for-byte the one `_convrot_w4a4_forward` makes, so numerics are
# unchanged -- this moves where the call is visible from, not what it computes.
#
# Built lazily and guarded: everything it depends on (the backend registry,
# `TensorCoreConvRotW4A4Layout.get_plain_tensors`, the `_params` field names) is
# comfy_kitchen's internal API, not a published one. If a kitchen update moves any of it,
# `_w4a4_op()` returns None and the loader falls back to `_shield_from_dynamo` rather than
# failing to load a checkpoint.
_W4A4_OP = None
_W4A4_OP_ERROR = None
# Backend resolution, memoized. `convrot_w4a4_linear` re-runs it on *every call*: it builds a
# seven-key dict, walks `["cuda", "triton", "eager"]` and revalidates seven ParamConstraints,
# with no caching anywhere in `BackendRegistry`. At 224 layers times 8 steps that is 1792
# resolutions per image, all of them answering the same question.
#
# The key is exactly what those constraints read -- dtypes, device and rank, since the only
# shape rule on `x` is `MinDims(2)` and the ones on `qweight`/`wscales` are satisfied
# identically by every layer in the model. Token count is deliberately *not* in the key: it
# never reaches a constraint, only kernel selection inside the implementation.
#
# Not invalidated, because the thing that would change the answer -- ComfyUI's
# `ck.registry.disable("cuda")` under a pre-cu130 torch -- happens once, at
# `comfy.quant_ops` import, long before any of this runs.
_IMPL_CACHE: dict = {}
def _resolve_impl(x, qweight, wscales, bias, convrot_groupsize, quant_group_size,
linear_dtype):
from comfy_kitchen.registry import registry as ck_registry
key = (x.dtype, x.device, x.ndim, qweight.dtype, wscales.dtype,
None if bias is None else bias.dtype,
convrot_groupsize, quant_group_size, linear_dtype)
impl = _IMPL_CACHE.get(key)
if impl is None:
impl = ck_registry.get_implementation("convrot_w4a4_linear", kwargs={
"x": x, "qweight": qweight, "wscales": wscales, "bias": bias,
"convrot_groupsize": convrot_groupsize,
"quant_group_size": quant_group_size, "linear_dtype": linear_dtype,
})
_IMPL_CACHE[key] = impl
return impl
def _w4a4_op():
"""The registered `krea2::w4a4_linear` op, or None if this kitchen build cannot host it."""
global _W4A4_OP, _W4A4_OP_ERROR
if _W4A4_OP is not None or _W4A4_OP_ERROR is not None:
return _W4A4_OP
try:
# Imported for its side effect on this try block: no kitchen registry means no
# implementation to dispatch to, and finding that out here is what makes the
# fallback a load-time decision instead of a crash on the first forward.
from comfy_kitchen.registry import registry as ck_registry # noqa: F401
@torch.library.custom_op("krea2::w4a4_linear", mutates_args=())
def w4a4_linear(x: torch.Tensor, qweight: torch.Tensor, wscales: torch.Tensor,
bias: torch.Tensor | None, convrot_groupsize: int,
quant_group_size: int, linear_dtype: str) -> torch.Tensor:
impl = _resolve_impl(x, qweight, wscales, bias, convrot_groupsize,
quant_group_size, linear_dtype)
return impl(x, qweight, wscales, bias=bias,
convrot_groupsize=convrot_groupsize,
quant_group_size=quant_group_size, linear_dtype=linear_dtype)
@w4a4_linear.register_fake
def _(x, qweight, wscales, bias, convrot_groupsize, quant_group_size, linear_dtype):
# qweight is [out_features, in_features // 2] -- int4 packed two to a byte, so
# the output width is its *row* count and cannot be read off the last dim.
return x.new_empty(x.shape[:-1] + (qweight.shape[0],))
_W4A4_OP = w4a4_linear
except Exception as exc:
_W4A4_OP_ERROR = "{}: {}".format(type(exc).__name__, exc)
logging.info("[krea2-svdquant] no compile-friendly kernel op (%s); falling back to "
"graph breaks around the quantized linears", _W4A4_OP_ERROR)
return _W4A4_OP
def _install_custom_op(module: torch.nn.Module) -> bool:
"""Route this Linear's matmul through `krea2::w4a4_linear`. True if it took.
The fast path deliberately handles only the case ComfyUI's own quantized forward calls
"quantized": no LoRA weight/bias function, weight resident on the input's device, not
forced to full precision. Those are the same conditions `comfy/ops.py` gates
`_use_quantized` on, and the reason is the same -- anything else means the weight is
being rewritten or staged per call, which an op holding plain tensors cannot see. All
of it is re-checked *per forward* rather than at load, because ComfyUI attaches lowvram
patches and offloads weights long after this runs; when a check fails the call goes to
the stock forward and is simply a graph break, i.e. no worse than the old behaviour.
`params.transposed` is checked once here rather than per call: a transposed weight makes
kitchen dequantize and run a bf16 linear (`_handle_convrot_w4a4_linear`), which is a
different computation, not a slower one. A checkpoint whose weights arrive transposed
should keep whatever kitchen does with it.
"""
# An escape hatch, and the only honest way to A/B this: the two paths cannot coexist in
# one process, so the comparison is one server run against another, and a flag is what
# makes those two runs differ by exactly this decision.
if os.environ.get("KREA2_W4A4_OP") == "0":
return False
# Idempotent: the loader installs this at load time, the compile-prep node can be asked
# to do it again on a model that already has it, and wrapping a wrapper would put the
# guard block on the hot path twice for no benefit.
if getattr(module, "_krea2_op_installed", False):
return True
op = _w4a4_op()
if op is None:
return False
try:
from comfy_kitchen.tensor.convrot_w4a4 import TensorCoreConvRotW4A4Layout
params = module.weight._params
if params.transposed:
return False
groupsize = int(params.convrot_groupsize)
quant_group_size = int(params.quant_group_size)
linear_dtype = str(params.linear_dtype)
except Exception as exc:
logging.debug("[krea2-svdquant] cannot read layout params off %s: %s",
type(module).__name__, exc)
return False
original = module.forward
get_plain = TensorCoreConvRotW4A4Layout.get_plain_tensors
def forward(x, *args, **kwargs):
weight = module.weight
if (args or kwargs or x.ndim < 2 or x.requires_grad
or module.weight_function or module.bias_function
or getattr(module, "comfy_force_cast_weights", False)
or getattr(module, "_full_precision_mm", False)
or weight._qdata.device != x.device):
return original(x, *args, **kwargs)
qweight, wscales = get_plain(weight)
bias = module.bias
if bias is not None and bias.dtype != x.dtype:
bias = bias.to(dtype=x.dtype)
# The op exists for Dynamo's benefit; an eager call gains nothing from routing
# through `torch.library` and can skip that dispatch. `is_compiling()` is
# constant-folded during tracing, so the compiled graph still gets the op node --
# the same mechanism ComfyUI uses to keep `run_every_op` out of compiled graphs
# (`comfy/ops.py`). Measured either way it is within noise in eager; what the
# memoized `_resolve_impl` below is worth is not (0.846 -> 0.833 s/step on a 3090 at
# 1024px, rank 256, against `KREA2_W4A4_OP=0`).
if torch.compiler.is_compiling():
return op(x, qweight, wscales, bias, groupsize, quant_group_size, linear_dtype)
impl = _resolve_impl(x, qweight, wscales, bias, groupsize, quant_group_size,
linear_dtype)
return impl(x, qweight, wscales, bias=bias, convrot_groupsize=groupsize,
quant_group_size=quant_group_size, linear_dtype=linear_dtype)
module.forward = forward
module._krea2_op_installed = True
return True
def load_svdquant_w4a4(path: str, model_options: dict | None = None,
compile_safe: bool = True):
sd, metadata = comfy.utils.load_torch_file(path, return_metadata=True)
# `comfy.sd.load_diffusion_model_state_dict` strips this prefix internally when it builds
# the module tree, but we walk that tree ourselves to attach branches, so we strip it too.
# `default=""` rather than raising: a loader must not blow up on a checkpoint with no
# blocks, and the "found no branches" error below says far more about what went wrong.
layer_prefix = detect_prefix(sd.keys(), default="")
branches: dict[str, dict[str, torch.Tensor]] = {}
for key in list(sd.keys()):
for suffix, slot in ((_L1, "l1"), (_L2, "l2")):
if key.endswith(suffix):
branches.setdefault(key[: -len(suffix)], {})[slot] = sd.pop(key)
break
if not branches:
raise ValueError(
"{} carries no svdq_l1/svdq_l2 tensors - it is a plain quantized checkpoint, "
"load it with UNETLoader instead".format(path)
)
# `disable_dynamic=True` pins this to the classic ModelPatcher. On current ComfyUI
# that is a no-op (`CoreModelPatcher is ModelPatcher`), but when upstream flips it to
# ModelPatcherDynamic the streaming patcher takes ownership of the weights via
# `load_model_weights(..., assign=patcher.is_dynamic())`, which we have not validated
# against the branch buffers. Keep it pinned until that path is tested; the
# diagnostics node reports which patcher is actually in use.
patcher = comfy.sd.load_diffusion_model_state_dict(
sd, model_options=model_options or {}, metadata=metadata, disable_dynamic=True
)
if patcher is None:
raise RuntimeError("could not detect a model in {}".format(path))
diffusion_model = patcher.model.diffusion_model
model_dtype = patcher.model.get_dtype()
attached = 0
via_op = 0
ranks: set[int] = set()
incomplete = []
for layer, parts in branches.items():
if "l1" not in parts or "l2" not in parts:
incomplete.append(layer)
continue
submodule_path = layer[len(layer_prefix):] if layer_prefix else layer
base = _get_submodule(diffusion_model, submodule_path)
# Store the factors in the dtype the model computes in. `add_low_rank` casts to
# `x.dtype` on every call, and the checkpoint holds bf16 while ComfyUI runs this model
# in fp16 on anything that cannot do bf16 in tensor cores -- Turing especially, but it
# is what our own 3090 picks too. That mismatch is a full conversion of both factors
# per layer per forward: measured 0.060 ms per layer at rank 256, 13.5 ms per step
# across the 224, ~1.6% of a step. Converting once at load makes the per-call cast a
# no-op. Same 2 bytes per element either way, so nothing in the memory accounting
# moves; a model re-saved out of ComfyUI now writes fp16 factors, which the loader
# reads back identically.
if parts["l1"].dtype != model_dtype:
parts["l1"] = parts["l1"].to(model_dtype)
parts["l2"] = parts["l2"].to(model_dtype)
if compile_safe:
# The op keeps the layer inside the graph; the shield takes it out of one. Only
# one of the two can be installed, and the op is tried first.
if _install_custom_op(base):
via_op += 1
else:
_shield_from_dynamo(base)
attach_branch(base, parts["l1"], parts["l2"])
# Collected per layer rather than read once off the first branch, so a checkpoint
# with a non-uniform rank budget reports honestly instead of quoting layer zero.
ranks.add(int(parts["l1"].shape[1]))
attached += 1
# Silently returning a patcher with no branches would hand back a plain quantized model
# dressed as an SVDQuant one -- same class of failure quantize_krea2.py hard-fails on.
if attached == 0:
raise RuntimeError(
"{}: found {} svdq_l1/svdq_l2 key pairs but attached none of them"
"{}. The checkpoint's layer names do not line up with this model "
"(detected prefix {!r}).".format(
path, len(branches),
"; {} were missing their other half".format(len(incomplete))
if incomplete else "",
layer_prefix)
)
# The branch buffers were registered after the patcher computed (and cached) its size,
# so drop the cache and let `model_size()` re-derive it from the state dict.
patcher.size = 0
install_mask_guard(patcher)
# Metadata is a newer addition; checkpoints published before it still load, with the
# rank recovered from the factor shape exactly as before. The shapes are the ground
# truth, so they win over a metadata value that disagrees with them.
meta = metadata or {}
rank_desc = (str(next(iter(ranks))) if len(ranks) == 1
else "{}-{} mixed".format(min(ranks), max(ranks)))
meta_rank = meta.get("krea2_svdquant_rank")
if meta_rank and len(ranks) == 1 and int(meta_rank) != next(iter(ranks)):
logging.warning("[krea2-svdquant] metadata says rank %s but the factors are rank %s; "
"trusting the factors", meta_rank, rank_desc)
variant = meta.get("krea2_svdquant_variant", "unknown")
# Which of the two compile strategies each layer got is worth stating on every load, not
# just when it fails: "torch.compile barely helped" and "the op path silently fell back
# to graph breaks" look identical from the outside otherwise.
if not compile_safe:
compile_desc = "compile shielding off"
elif via_op == attached:
compile_desc = "compile: {} layers in-graph via krea2::w4a4_linear".format(via_op)
elif via_op:
compile_desc = "compile: {} layers in-graph, {} as graph breaks".format(
via_op, attached - via_op)
else:
compile_desc = "compile: all {} layers are graph breaks{}".format(
attached, " ({})".format(_W4A4_OP_ERROR) if _W4A4_OP_ERROR else "")
summary = ("w4a4 + low-rank: attached {} branches (rank {}, variant {}), "
"model_size {:.2f} GiB, {}".format(
attached, rank_desc, variant, patcher.model_size() / 1024 ** 3,
compile_desc))
logging.info("[krea2-svdquant] %s", summary)
dispatch = log_dispatch(diffusion_model)
# Stashed rather than returned so callers that just want the model (diagnose.py, the
# head-to-head scripts) keep working unchanged. The node surfaces it in the UI, which
# matters most for the dispatch warning: buried in the console, the people who most need
# to read it are exactly the ones who never see it.
patcher.krea2_load_summary = "\n".join(x for x in (summary, dispatch) if x)
return patcher
class Krea2SVDQuantW4A4Loader:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model_name": (folder_paths.get_filename_list("diffusion_models"), {
"tooltip": "A checkpoint from quantize_krea2.py --format svdq (it carries "
"*.svdq_l1/*.svdq_l2 tensors). The --format w4a4 / int8 / fp8 "
"checkpoints have no branch and load with the stock UNETLoader "
"instead.",
}),
}
}
RETURN_TYPES = ("MODEL", "STRING")
RETURN_NAMES = ("model", "status")
OUTPUT_TOOLTIPS = ("Wire this to a KSampler.",
"Rank, variant, size and which kernel the quantized layers will "
"actually dispatch to. Read this if generation is slow.")
OUTPUT_NODE = True
FUNCTION = "load"
CATEGORY = _CATEGORY
TITLE = "Krea2 SVDQuant W4A4 Loader"
DESCRIPTION = ("Loads a W4A4 + low-rank (SVDQuant) Krea2 checkpoint. Self-contained: "
"no separate base model needed. The status output tells you whether the "
"fast int4 kernel is in play.")
def load(self, model_name):
path = folder_paths.get_full_path_or_raise("diffusion_models", model_name)
patcher = load_svdquant_w4a4(path)
status = getattr(patcher, "krea2_load_summary", "")
return {"ui": {"text": [status]}, "result": (patcher, status)}
NODE_CLASS_MAPPINGS = {"Krea2SVDQuantW4A4Loader": Krea2SVDQuantW4A4Loader}
NODE_DISPLAY_NAME_MAPPINGS = {"Krea2SVDQuantW4A4Loader": "Krea2 SVDQuant W4A4 Loader"}
|