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
| """Bake a LoRA into a Krea2 checkpoint *before* it is quantized. | |
| python tools/bake_adapter.py raw.safetensors --lora Krea2/realism_engine.safetensors \ | |
| --format svdq --rank 256 --variant base --act-stats krea2_act_stats_base.safetensors | |
| Why this exists. A LoKr, LoHa or OFT cannot fold into the low-rank branch, so the node has | |
| two choices at runtime and neither is free: compute the adapter every forward (exact, and on | |
| a 3090 at 1440x1920 that is +1.8 s per model call), or hand it to ComfyUI, which rewrites the | |
| 4-bit weight and requantizes the LoRA delta along with it (free, and lossy). | |
| Baking before quantization avoids the choice. The delta is added to the **bf16** weight, and | |
| then the SVDQuant split runs on the merged weight -- so the low-rank branch is fitted against | |
| what you will actually sample with, and absorbs the error that the naive | |
| dequantize-add-requantize path throws away. At runtime the LoRA is simply gone: no adapter, | |
| no branch of its own, no per-step cost at all. | |
| The delta is computed by ComfyUI's own weight adapters (`adapter.calculate_weight`), so every | |
| format and naming convention ComfyUI supports works here, alpha and dora_scale included. | |
| The merged weights are never written out whole -- the source is 24 GB -- they are streamed | |
| into `convert()` through its `weight_patch` hook. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import sys | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, os.path.dirname(HERE)) # the node package | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(HERE)))) # ComfyUI root | |
| import torch # noqa: E402 | |
| import comfy.lora # noqa: E402 | |
| import comfy.utils # noqa: E402 | |
| import comfy.weight_adapter # noqa: E402 | |
| from quantize_krea2 import ( # noqa: E402 | |
| LAYER_PREFIXES, | |
| convert, | |
| derive_out_path, | |
| detect_prefix, | |
| resolve_format, | |
| ) | |
| _PREFIX = "diffusion_model." | |
| _ALT_PREFIX = "transformer." | |
| def build_key_map(keys, prefix: str) -> dict: | |
| """`{lora key: checkpoint key}` for a bare diffusion-model state dict. | |
| ComfyUI's `model_lora_keys_unet` needs a live model; here there is only a file. The | |
| mapping it would produce for the native names is mechanical, and the diffusers names come | |
| from the same table ComfyUI uses (`krea2_to_diffusers`), so both stay in step with it. | |
| """ | |
| key_map = {} | |
| for key in keys: | |
| if not key.endswith(".weight"): | |
| continue | |
| name = key[: -len(".weight")] | |
| bare = name[len(prefix):] if prefix and name.startswith(prefix) else name | |
| for alias in (_PREFIX + bare, _ALT_PREFIX + bare, bare): | |
| key_map.setdefault(alias, key) | |
| to_diffusers = getattr(comfy.utils, "krea2_to_diffusers", None) | |
| if to_diffusers is not None: | |
| # `unet_config` only has to carry the layer counts the map is built from; the real | |
| # config is not on disk next to the weights. | |
| cfg = {"layers": sum(1 for k in keys if k.endswith("blocks.0.attn.wq.weight")) and 28} | |
| try: | |
| for lora_key, model_key in to_diffusers(cfg, output_prefix=prefix).items(): | |
| if model_key in keys and lora_key.endswith(".weight"): | |
| stem = lora_key[: -len(".weight")] | |
| key_map.setdefault(_PREFIX + stem, model_key) | |
| key_map.setdefault(_ALT_PREFIX + stem, model_key) | |
| key_map.setdefault(stem, model_key) | |
| except Exception as exc: # a config mismatch is not fatal here | |
| print("note: diffusers key map unavailable ({})".format(exc), flush=True) | |
| return key_map | |
| def load_patches(source_keys, prefix, loras): | |
| """`{checkpoint key: [(adapter, strength), ...]}` for every LoRA on the command line.""" | |
| key_map = build_key_map(source_keys, prefix) | |
| patches: dict[str, list] = {} | |
| for path, strength in loras: | |
| sd = comfy.utils.load_torch_file(path, safe_load=True) | |
| loaded = comfy.lora.load_lora(sd, key_map, log_missing=False) | |
| if not loaded: | |
| raise SystemExit("no layer of {} matched this checkpoint".format(path)) | |
| for key, patch in loaded.items(): | |
| patches.setdefault(key, []).append((patch, strength)) | |
| print(" {}: {} layers @ {:.2f}".format(os.path.basename(path), len(loaded), strength), | |
| flush=True) | |
| return patches | |
| def make_weight_patch(patches: dict, applied: dict): | |
| """The `convert()` hook: weight -> weight + sum of the LoRA deltas for that key.""" | |
| def weight_patch(key: str, tensor: torch.Tensor) -> torch.Tensor: | |
| entries = patches.get(key) | |
| if not entries: | |
| return tensor | |
| # fp32 for the accumulation: the deltas are small next to the weight, and this is a | |
| # one-off cost paid at build time rather than per step. `copy=True` because | |
| # `calculate_weight` adds in place and `.to()` is a no-op on a tensor that is already | |
| # fp32 -- without it this would mutate the caller's tensor. | |
| merged = tensor.to(torch.float32, copy=True) | |
| for adapter, strength in entries: | |
| # `offset` is None when the patch covers the whole weight; passing 0 makes | |
| # ComfyUI subscript it (`offset[0]`) and die -- and it dies on the *last* key, | |
| # after the quantizer has already burned a quarter of an hour. | |
| if isinstance(adapter, comfy.weight_adapter.WeightAdapterBase): | |
| merged = adapter.calculate_weight( | |
| merged, key, strength, strength, None, lambda x: x, | |
| intermediate_dtype=torch.float32) | |
| else: | |
| merged = comfy.lora.calculate_weight( | |
| [(strength, adapter, strength, None, None)], merged, key, | |
| intermediate_dtype=torch.float32) | |
| applied[key] = applied.get(key, 0) + len(entries) | |
| return merged.to(tensor.dtype) | |
| return weight_patch | |
| def preflight(src: str, patches: dict) -> None: | |
| """Apply one patch of each kind for real, before the quantizer runs for a quarter hour. | |
| The quantizer processes the block weights first and the plain ones last, so a mistake in | |
| how a `diff` patch is applied surfaces only after every layer has been quantized. One | |
| tensor per distinct patch type costs a couple of seconds and moves that to the start. | |
| """ | |
| from safetensors import safe_open | |
| seen = set() | |
| probes = [] | |
| for key, entries in patches.items(): | |
| kind = tuple(type(a).__name__ for a, _ in entries) | |
| if kind not in seen: | |
| seen.add(kind) | |
| probes.append(key) | |
| with safe_open(src, framework="pt", device="cpu") as handle: | |
| for key in probes: | |
| hook = make_weight_patch(patches, {}) | |
| hook(key, handle.get_tensor(key)) | |
| print(" preflight ok ({} patch shape(s))".format(len(probes)), flush=True) | |
| def main(): | |
| ap = argparse.ArgumentParser(description=__doc__, | |
| formatter_class=argparse.RawDescriptionHelpFormatter) | |
| ap.add_argument("src", help="the high-precision checkpoint (raw.safetensors, turbo...)") | |
| ap.add_argument("--lora", action="append", required=True, metavar="PATH[:STRENGTH]", | |
| help="repeat to bake several, in order; strength defaults to 1.0") | |
| ap.add_argument("--format", choices=["int8", "w4a4", "svdq", "fp8"], default="svdq") | |
| ap.add_argument("--groupsize", type=int, default=256) | |
| ap.add_argument("--rank", type=int, default=256) | |
| ap.add_argument("--rank-alloc", default="uniform") | |
| ap.add_argument("--refine-iters", type=int, default=100) | |
| ap.add_argument("--variant", choices=["turbo", "base", "unknown"], default="unknown") | |
| ap.add_argument("--act-stats", default=None) | |
| ap.add_argument("--device", default="cuda") | |
| ap.add_argument("--out", default=None) | |
| args = ap.parse_args() | |
| loras = [] | |
| for spec in args.lora: | |
| path, _, strength = spec.rpartition(":") | |
| # A bare Windows path has a colon in it ("D:/loras/x.safetensors"), so only treat the | |
| # tail as a strength when it actually parses as one. | |
| try: | |
| loras.append((path, float(strength)) if path else (spec, 1.0)) | |
| except ValueError: | |
| loras.append((spec, 1.0)) | |
| fmt, rank = resolve_format(args.format, args.rank, rank_was_set=True) | |
| from safetensors import safe_open | |
| with safe_open(args.src, framework="pt", device="cpu") as handle: | |
| keys = list(handle.keys()) | |
| prefix = detect_prefix(keys, default=LAYER_PREFIXES[0]) | |
| print("baking {} adapter(s) into {}".format(len(loras), os.path.basename(args.src)), | |
| flush=True) | |
| patches = load_patches(keys, prefix, loras) | |
| applied: dict[str, int] = {} | |
| preflight(args.src, patches) | |
| out = args.out | |
| if out is None: | |
| out, _ = derive_out_path(args.src, args.format, rank, args.variant, args.rank_alloc, | |
| args.act_stats) | |
| stem, ext = os.path.splitext(out) | |
| out = "{}-baked{}".format(stem, ext) | |
| convert(args.src, out, fmt, args.groupsize, args.device, rank, args.refine_iters, | |
| variant=args.variant, rank_alloc=args.rank_alloc, act_stats=args.act_stats, | |
| weight_patch=make_weight_patch(patches, applied)) | |
| missing = set(patches) - set(applied) | |
| if missing: | |
| # Silence here would mean shipping a checkpoint that is quietly missing part of the | |
| # LoRA, which is exactly the failure this tool exists to avoid. | |
| raise SystemExit("{} matched layer(s) never reached the writer: {}".format( | |
| len(missing), ", ".join(sorted(missing)[:5]))) | |
| print("baked {} layer(s) into {}".format(len(applied), out)) | |
| if __name__ == "__main__": | |
| main() | |