ostris commited on
Commit
827ab51
·
verified ·
1 Parent(s): 6e739db

Upload 5 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ images/out_style_yeti_clean.png filter=lfs diff=lfs merge=lfs -text
37
+ images/out_yeti_no_ref.png filter=lfs diff=lfs merge=lfs -text
38
+ images/style_ref_clean.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: diffusers
3
+ base_model: krea/Krea-2-Turbo
4
+ tags:
5
+ - text-to-image
6
+ - image-to-image
7
+ - custom-pipeline
8
+ - krea2
9
+ - lora
10
+ license: apache-2.0
11
+ ---
12
+
13
+ # Krea2OstrisEdit
14
+
15
+ A self-contained [community pipeline](https://huggingface.co/docs/diffusers/main/en/using-diffusers/custom_pipeline_overview) for [Krea 2](https://huggingface.co/krea/Krea-2-Turbo) that adds:
16
+
17
+ - **Reference-image (edit) conditioning** — pass 1–2 reference images and the model generates with them as context (style transfer, editing, subject reference, etc., depending on the LoRA you load). This matches how edit LoRAs are trained with [AI Toolkit](https://github.com/ostris/ai-toolkit)'s Krea 2 reference-image trainer and how they run with the [ComfyUI-Krea2-Ostris-Edit](https://github.com/ostris/ComfyUI-Krea2-Ostris-Edit) custom nodes.
18
+ - **LoRA loading** for AI Toolkit / ComfyUI-format Krea 2 LoRAs (`diffusion_model.*` keys, `lora_A/lora_B` or `lora_down/lora_up` + alpha) as well as diffusers-format state dicts.
19
+
20
+ Everything lives in a single `pipeline.py`, so it works on diffusers releases that don't ship Krea 2 yet. Without a reference image it is a plain Krea 2 text-to-image sampler.
21
+
22
+ | Reference | Output | Same seed, no reference |
23
+ | :---: | :---: | :---: |
24
+ | ![reference](images/style_ref_clean.png) | ![output](images/out_style_yeti_clean.png) | ![no reference](images/out_yeti_no_ref.png) |
25
+
26
+ *"a white yeti with horns reading a book" with the [Style Reference LoRA](https://huggingface.co/ostris/krea2_turbo_style_reference) — the reference image drives the style.*
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ import torch
32
+ from diffusers import DiffusionPipeline
33
+ from PIL import Image
34
+
35
+ pipe = DiffusionPipeline.from_pretrained(
36
+ "krea/Krea-2-Turbo",
37
+ custom_pipeline="ostris/Krea2OstrisEdit",
38
+ torch_dtype=torch.bfloat16,
39
+ )
40
+ pipe.enable_model_cpu_offload() # or pipe.to("cuda") with ~40+ GB of VRAM
41
+
42
+ # An AI-Toolkit Krea 2 LoRA, e.g. the style reference LoRA
43
+ pipe.load_lora_weights(
44
+ "ostris/krea2_turbo_style_reference", weight_name="krea2_style_reference.safetensors"
45
+ )
46
+
47
+ image = pipe(
48
+ "a white yeti with horns reading a book",
49
+ image=Image.open("style_reference.png"), # one reference image or a list of them
50
+ ).images[0]
51
+ image.save("output.png")
52
+ ```
53
+
54
+ Works the same with `krea/Krea-2-Raw` (the non-distilled base model); sampling defaults adapt automatically (see below).
55
+
56
+ ## Call arguments
57
+
58
+ Beyond the standard diffusers text-to-image arguments (`prompt`, `negative_prompt`, `height`, `width`, `num_inference_steps`, `guidance_scale`, `generator`, ...):
59
+
60
+ | Argument | Default | Description |
61
+ | --- | --- | --- |
62
+ | `image` | `None` | Reference image(s): a PIL image, numpy array, `[0,1]` CHW tensor, or a list of them. References keep their own aspect ratio; output size is set by `height`/`width` independently. |
63
+ | `reference_max_pixels` | `1024 * 1024` | Pixel budget each reference is downscaled to fit (never upscaled) before VAE encoding. |
64
+ | `vl_image_max_pixels` | `384 * 384` | Pixel budget for the coarse Qwen3-VL view of each reference. |
65
+ | `encode_reference_in_prompt` | `True` | Also embed references into the text conditioning through the Qwen3-VL vision tower (matches AI-Toolkit edit training). |
66
+ | `max_sequence_length` | `512` | Maximum prompt token length (truncation only; prompts are encoded at natural length, not padded). |
67
+
68
+ Defaults for `num_inference_steps` / `guidance_scale` follow the loaded checkpoint: **8 / 0.0** for the distilled Turbo model, **28 / 4.5** for the base model. Guidance uses the Krea 2 convention `cond + scale * (cond - uncond)`, enabled whenever `scale > 0` (this equals standard CFG with scale `1 + scale`).
69
+
70
+ ## How reference conditioning works
71
+
72
+ Reference images condition the model in two places:
73
+
74
+ 1. **Through the Qwen3-VL text encoder** — each image is embedded in the user message ahead of the prompt via `Picture N: <|vision_start|><|image_pad|><|vision_end|>` placeholders, so the text embeddings "see" the references.
75
+ 2. **As clean VAE latents** appended after the noisy image tokens in the transformer sequence. They keep flow time `t=0` (they are never noised) and sit on rotary-position frame axis `i + 1` — the Kontext-style "index" placement.
76
+
77
+ ## LoRA support
78
+
79
+ `pipe.load_lora_weights(...)` accepts a hub repo id (+ `weight_name`), a local `.safetensors` file or directory, or a state dict, in any of these formats:
80
+
81
+ - AI Toolkit / reference-trainer keys: `diffusion_model.blocks.N.attn.wq.lora_A.weight`, ...
82
+ - ComfyUI-style `lora_down.weight` / `lora_up.weight` with optional `.alpha` tensors (folded into the effective scale)
83
+ - Already-converted diffusers keys: `transformer.transformer_blocks.N.attn.to_q.lora_A.weight`, ...
84
+
85
+ `unload_lora_weights()`, `fuse_lora()` / `unfuse_lora()`, `set_adapters()`, and per-call scaling via `attention_kwargs={"scale": 0.8}` are also available.
86
+
87
+ ## Hardware notes
88
+
89
+ - bf16 weights are ~24 GB (transformer) + ~8 GB (Qwen3-VL text encoder) + VAE, so use `pipe.enable_model_cpu_offload()` on cards with less than ~40 GB of VRAM. On a 32 GB RTX 5090 a 1024×1024 Turbo image takes ~40–50 s with offloading.
90
+ - The Qwen3-VL image processor (only needed when passing reference images) is lazily loaded from `Qwen/Qwen3-VL-4B-Instruct`.
91
+
92
+ ## License
93
+
94
+ The pipeline code is Apache-2.0 (portions of the transformer implementation adapted from [huggingface/diffusers](https://github.com/huggingface/diffusers)). The Krea 2 model weights are covered by the [Krea 2 Community License](https://huggingface.co/krea/Krea-2-Turbo/blob/main/LICENSE.pdf).
images/out_style_yeti_clean.png ADDED

Git LFS Details

  • SHA256: 3580f441a7ee65fde1c59e56aee3d9780e404c3575231ab8af96168568c4b47e
  • Pointer size: 132 Bytes
  • Size of remote file: 2.11 MB
images/out_yeti_no_ref.png ADDED

Git LFS Details

  • SHA256: db920d6bdd15d4996bc8a3159c49a9403e9ae3d2748a4df52f0f2f7739fd14e9
  • Pointer size: 132 Bytes
  • Size of remote file: 1.19 MB
images/style_ref_clean.png ADDED

Git LFS Details

  • SHA256: dd36b3495e36eb94460f9d6d403ea6b9926244ab3f940074a9a214f92e68fc69
  • Pointer size: 132 Bytes
  • Size of remote file: 2.09 MB
pipeline.py ADDED
@@ -0,0 +1,1376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Ostris, LLC. All rights reserved.
2
+ #
3
+ # Portions of the Krea2Transformer2DModel implementation are adapted from
4
+ # huggingface/diffusers (Apache License, Version 2.0), Copyright 2026 Krea AI
5
+ # and The HuggingFace Team.
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ """Krea2OstrisEdit -- a self-contained Hugging Face community pipeline for Krea 2
19
+ with reference-image (edit) conditioning and Ostris AI-Toolkit LoRA loading.
20
+
21
+ Everything lives in this one file so it can be hosted as a hub community
22
+ pipeline (a model repo containing just this ``pipeline.py``):
23
+
24
+ ```python
25
+ import torch
26
+ from diffusers import DiffusionPipeline
27
+ from PIL import Image
28
+
29
+ pipe = DiffusionPipeline.from_pretrained(
30
+ "krea/Krea-2-Turbo",
31
+ custom_pipeline="ostris/Krea2OstrisEdit",
32
+ torch_dtype=torch.bfloat16,
33
+ )
34
+ pipe.to("cuda") # or pipe.enable_model_cpu_offload() on GPUs with < ~40 GB VRAM
35
+
36
+ # Load an AI-Toolkit (or already-diffusers-format) Krea 2 LoRA, e.g. the style
37
+ # reference LoRA (generates the prompt in the style of the reference images).
38
+ pipe.load_lora_weights(
39
+ "ostris/krea2_turbo_style_reference", weight_name="krea2_style_reference.safetensors"
40
+ )
41
+
42
+ image = pipe(
43
+ "a white yeti with horns reading a book",
44
+ image=Image.open("style_reference.png"), # one reference image or a list of them
45
+ num_inference_steps=8, # Turbo defaults; the base model wants 28 / 4.5
46
+ guidance_scale=0.0,
47
+ ).images[0]
48
+ image.save("output.png")
49
+ ```
50
+
51
+ Reference images condition the model in two places, matching how the edit LoRAs
52
+ are trained with Ostris AI-Toolkit (and the ComfyUI-Krea2-Ostris-Edit nodes):
53
+
54
+ 1. through the Qwen3-VL text encoder: each image is embedded in the user message
55
+ ahead of the prompt via ``Picture N: <|vision_start|><|image_pad|><|vision_end|>``
56
+ placeholders, so the text embeddings "see" the references;
57
+ 2. as clean VAE latents appended after the noisy image tokens in the transformer
58
+ sequence. They keep the flow time ``t=0`` (they are never noised) and sit on
59
+ rotary-position frame axis ``i + 1`` -- the Kontext-style "index" placement.
60
+
61
+ Without ``image`` the pipeline is a plain Krea 2 text-to-image sampler.
62
+ """
63
+
64
+ import math
65
+ import os
66
+ import re
67
+ from dataclasses import dataclass
68
+ from typing import Any, Dict, List, Optional, Tuple, Union
69
+
70
+ import numpy as np
71
+ import PIL.Image
72
+ import torch
73
+ import torch.nn as nn
74
+ import torch.nn.functional as F
75
+
76
+ import diffusers
77
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
78
+ from diffusers.image_processor import VaeImageProcessor
79
+ from diffusers.loaders import PeftAdapterMixin
80
+ from diffusers.models import AutoencoderKLQwenImage
81
+ from diffusers.models.modeling_outputs import Transformer2DModelOutput
82
+ from diffusers.models.modeling_utils import ModelMixin
83
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline
84
+ from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
85
+ from diffusers.utils import USE_PEFT_BACKEND, BaseOutput, logging, scale_lora_layers, unscale_lora_layers
86
+ from diffusers.utils.torch_utils import randn_tensor
87
+
88
+ try:
89
+ from transformers import AutoTokenizer, Qwen3VLModel
90
+ except ImportError as e: # pragma: no cover
91
+ raise ImportError(
92
+ "Krea2OstrisEdit requires a transformers version that ships Qwen3-VL "
93
+ "(`transformers>=4.57`). Please upgrade transformers."
94
+ ) from e
95
+
96
+
97
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
98
+
99
+ # torch>=2.5 supports grouped-query attention natively in SDPA; older versions
100
+ # need the key/value heads repeated to the query head count.
101
+ _SDPA_HAS_GQA = tuple(int(re.sub(r"\D.*", "", v) or 0) for v in torch.__version__.split(".")[:2]) >= (2, 5)
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Transformer (Krea 2 single-stream MMDiT)
106
+ #
107
+ # Module tree and state-dict keys match the `Krea2Transformer2DModel` checkpoint
108
+ # layout in the `transformer/` folder of the Krea 2 hub repos, so the sharded
109
+ # weights load directly. The forward pass additionally supports clean reference
110
+ # tokens appended after the image tokens (`ref_seq_len`), which are modulated at
111
+ # flow time t=0 while the text + noisy image tokens keep the real timestep.
112
+ # ---------------------------------------------------------------------------
113
+
114
+
115
+ class Krea2RMSNorm(nn.Module):
116
+ """RMSNorm with a zero-centered scale: the effective multiplier is ``1 + weight``,
117
+ matching the Krea 2 checkpoint format. Normalization runs in float32."""
118
+
119
+ def __init__(self, dim: int, eps: float = 1e-5) -> None:
120
+ super().__init__()
121
+ self.dim = dim
122
+ self.eps = eps
123
+ self.weight = nn.Parameter(torch.zeros(dim))
124
+
125
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
126
+ dtype = hidden_states.dtype
127
+ hidden_states = F.rms_norm(
128
+ hidden_states.float(), (self.dim,), weight=self.weight.float() + 1.0, eps=self.eps
129
+ )
130
+ return hidden_states.to(dtype)
131
+
132
+
133
+ def _apply_rotary_emb(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
134
+ """Rotate interleaved (even, odd) channel pairs. ``x`` is (B, H, S, D); ``cos``/``sin``
135
+ are (S, D) in the repeat-interleaved layout produced by ``Krea2RotaryPosEmbed``."""
136
+ x_f = x.float()
137
+ x_rot = torch.stack((-x_f[..., 1::2], x_f[..., 0::2]), dim=-1).flatten(-2)
138
+ return (x_f * cos + x_rot * sin).to(x.dtype)
139
+
140
+
141
+ class Krea2RotaryPosEmbed(nn.Module):
142
+ def __init__(self, theta: float, axes_dim: List[int]) -> None:
143
+ super().__init__()
144
+ self.theta = theta
145
+ self.axes_dim = axes_dim
146
+
147
+ def forward(self, ids: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
148
+ # ids: (seq_len, 3) rotary coordinates. Frequencies are computed in float64
149
+ # (float32 on backends without float64 support, e.g. MPS).
150
+ dtype = torch.float32 if ids.device.type == "mps" else torch.float64
151
+ angles = []
152
+ for i, dim in enumerate(self.axes_dim):
153
+ pos = ids[:, i].to(dtype)
154
+ freqs = 1.0 / (self.theta ** (torch.arange(0, dim, 2, dtype=dtype, device=ids.device) / dim))
155
+ angles.append(pos[:, None] * freqs[None, :])
156
+ angles = torch.cat(angles, dim=-1)
157
+ cos = angles.cos().repeat_interleave(2, dim=-1).float()
158
+ sin = angles.sin().repeat_interleave(2, dim=-1).float()
159
+ return cos, sin
160
+
161
+
162
+ class Krea2Attention(nn.Module):
163
+ """Self-attention with grouped-query projections, q/k RMSNorm, rotary embeddings
164
+ and a sigmoid output gate."""
165
+
166
+ def __init__(self, hidden_size: int, num_heads: int, num_kv_heads: Optional[int] = None, eps: float = 1e-5):
167
+ super().__init__()
168
+ self.num_heads = num_heads
169
+ self.num_kv_heads = num_kv_heads if num_kv_heads is not None else num_heads
170
+ self.head_dim = hidden_size // num_heads
171
+
172
+ self.to_q = nn.Linear(hidden_size, self.head_dim * self.num_heads, bias=False)
173
+ self.to_k = nn.Linear(hidden_size, self.head_dim * self.num_kv_heads, bias=False)
174
+ self.to_v = nn.Linear(hidden_size, self.head_dim * self.num_kv_heads, bias=False)
175
+ self.to_gate = nn.Linear(hidden_size, hidden_size, bias=False)
176
+ self.norm_q = Krea2RMSNorm(self.head_dim, eps=eps)
177
+ self.norm_k = Krea2RMSNorm(self.head_dim, eps=eps)
178
+ self.to_out = nn.ModuleList([nn.Linear(hidden_size, hidden_size, bias=False), nn.Dropout(0.0)])
179
+
180
+ def forward(
181
+ self,
182
+ hidden_states: torch.Tensor,
183
+ attention_mask: Optional[torch.Tensor] = None,
184
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
185
+ ) -> torch.Tensor:
186
+ query = self.to_q(hidden_states).unflatten(-1, (self.num_heads, self.head_dim)).transpose(1, 2)
187
+ key = self.to_k(hidden_states).unflatten(-1, (self.num_kv_heads, self.head_dim)).transpose(1, 2)
188
+ value = self.to_v(hidden_states).unflatten(-1, (self.num_kv_heads, self.head_dim)).transpose(1, 2)
189
+ gate = self.to_gate(hidden_states)
190
+
191
+ query = self.norm_q(query)
192
+ key = self.norm_k(key)
193
+
194
+ if image_rotary_emb is not None:
195
+ cos, sin = image_rotary_emb
196
+ query = _apply_rotary_emb(query, cos, sin)
197
+ key = _apply_rotary_emb(key, cos, sin)
198
+
199
+ is_gqa = self.num_heads != self.num_kv_heads
200
+ if is_gqa and not _SDPA_HAS_GQA:
201
+ key = key.repeat_interleave(self.num_heads // self.num_kv_heads, dim=1)
202
+ value = value.repeat_interleave(self.num_heads // self.num_kv_heads, dim=1)
203
+ sdpa_kwargs = {"enable_gqa": True} if (is_gqa and _SDPA_HAS_GQA) else {}
204
+ hidden_states = F.scaled_dot_product_attention(query, key, value, attn_mask=attention_mask, **sdpa_kwargs)
205
+
206
+ hidden_states = hidden_states.transpose(1, 2).flatten(2)
207
+ hidden_states = hidden_states * torch.sigmoid(gate)
208
+ return self.to_out[0](hidden_states)
209
+
210
+
211
+ class Krea2SwiGLU(nn.Module):
212
+ def __init__(self, dim: int, hidden_dim: int) -> None:
213
+ super().__init__()
214
+ self.gate = nn.Linear(dim, hidden_dim, bias=False)
215
+ self.up = nn.Linear(dim, hidden_dim, bias=False)
216
+ self.down = nn.Linear(hidden_dim, dim, bias=False)
217
+
218
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
219
+ return self.down(F.silu(self.gate(hidden_states)) * self.up(hidden_states))
220
+
221
+
222
+ class Krea2TextFusionBlock(nn.Module):
223
+ """Pre-norm transformer block (no rotary embeddings, no time modulation) used by
224
+ the text fusion stage."""
225
+
226
+ def __init__(self, dim: int, num_heads: int, num_kv_heads: int, intermediate_size: int, eps: float) -> None:
227
+ super().__init__()
228
+ self.norm1 = Krea2RMSNorm(dim, eps=eps)
229
+ self.norm2 = Krea2RMSNorm(dim, eps=eps)
230
+ self.attn = Krea2Attention(dim, num_heads, num_kv_heads, eps=eps)
231
+ self.ff = Krea2SwiGLU(dim, intermediate_size)
232
+
233
+ def forward(self, hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
234
+ hidden_states = hidden_states + self.attn(self.norm1(hidden_states), attention_mask=attention_mask)
235
+ hidden_states = hidden_states + self.ff(self.norm2(hidden_states))
236
+ return hidden_states
237
+
238
+
239
+ class Krea2TextFusion(nn.Module):
240
+ """Fuses the stack of tapped text-encoder hidden states into one text sequence:
241
+ ``layerwise_blocks`` attend across the layer axis per token, a linear ``projector``
242
+ collapses that axis, and ``refiner_blocks`` attend across the token sequence."""
243
+
244
+ def __init__(
245
+ self,
246
+ num_text_layers: int,
247
+ dim: int,
248
+ num_heads: int,
249
+ num_kv_heads: int,
250
+ intermediate_size: int,
251
+ num_layerwise_blocks: int,
252
+ num_refiner_blocks: int,
253
+ eps: float,
254
+ ) -> None:
255
+ super().__init__()
256
+ self.layerwise_blocks = nn.ModuleList(
257
+ [
258
+ Krea2TextFusionBlock(dim, num_heads, num_kv_heads, intermediate_size, eps)
259
+ for _ in range(num_layerwise_blocks)
260
+ ]
261
+ )
262
+ self.projector = nn.Linear(num_text_layers, 1, bias=False)
263
+ self.refiner_blocks = nn.ModuleList(
264
+ [
265
+ Krea2TextFusionBlock(dim, num_heads, num_kv_heads, intermediate_size, eps)
266
+ for _ in range(num_refiner_blocks)
267
+ ]
268
+ )
269
+
270
+ def forward(self, encoder_hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor] = None):
271
+ batch_size, seq_len, num_text_layers, dim = encoder_hidden_states.shape
272
+
273
+ hidden_states = encoder_hidden_states.reshape(batch_size * seq_len, num_text_layers, dim)
274
+ for block in self.layerwise_blocks:
275
+ hidden_states = block(hidden_states.contiguous())
276
+
277
+ hidden_states = hidden_states.reshape(batch_size, seq_len, num_text_layers, dim).permute(0, 1, 3, 2)
278
+ hidden_states = self.projector(hidden_states).squeeze(-1)
279
+
280
+ for block in self.refiner_blocks:
281
+ hidden_states = block(hidden_states, attention_mask=attention_mask)
282
+
283
+ return hidden_states
284
+
285
+
286
+ class Krea2TransformerBlock(nn.Module):
287
+ def __init__(
288
+ self, hidden_size: int, intermediate_size: int, num_heads: int, num_kv_heads: int, norm_eps: float
289
+ ) -> None:
290
+ super().__init__()
291
+ self.scale_shift_table = nn.Parameter(torch.zeros(6, hidden_size))
292
+ self.norm1 = Krea2RMSNorm(hidden_size, eps=norm_eps)
293
+ self.norm2 = Krea2RMSNorm(hidden_size, eps=norm_eps)
294
+ self.attn = Krea2Attention(hidden_size, num_heads, num_kv_heads, eps=norm_eps)
295
+ self.ff = Krea2SwiGLU(hidden_size, intermediate_size)
296
+
297
+ def forward(
298
+ self,
299
+ hidden_states: torch.Tensor,
300
+ temb: Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor, int]],
301
+ image_rotary_emb: Tuple[torch.Tensor, torch.Tensor],
302
+ attention_mask: Optional[torch.Tensor] = None,
303
+ ) -> torch.Tensor:
304
+ # ``temb`` is the (B, 1, 6 * hidden_size) modulation input, or a tuple
305
+ # ``(temb, ref_temb, split)`` for reference-image conditioning: tokens
306
+ # ``[:split]`` (text + noisy image) are modulated with the real timestep
307
+ # while tokens ``[split:]`` (clean reference tokens) use the t=0 embedding.
308
+ if isinstance(temb, tuple):
309
+ temb, ref_temb, split = temb
310
+ m = (temb.unflatten(-1, (6, -1)) + self.scale_shift_table).unbind(-2)
311
+ r = (ref_temb.unflatten(-1, (6, -1)) + self.scale_shift_table).unbind(-2)
312
+
313
+ def modulate(h, scale_idx, shift_idx):
314
+ return torch.cat(
315
+ (
316
+ (1.0 + m[scale_idx]) * h[:, :split] + m[shift_idx],
317
+ (1.0 + r[scale_idx]) * h[:, split:] + r[shift_idx],
318
+ ),
319
+ dim=1,
320
+ )
321
+
322
+ def gate(h, gate_idx):
323
+ return torch.cat((m[gate_idx] * h[:, :split], r[gate_idx] * h[:, split:]), dim=1)
324
+
325
+ attn_out = self.attn(
326
+ modulate(self.norm1(hidden_states), 0, 1),
327
+ attention_mask=attention_mask,
328
+ image_rotary_emb=image_rotary_emb,
329
+ )
330
+ hidden_states = hidden_states + gate(attn_out, 2)
331
+ ff_out = self.ff(modulate(self.norm2(hidden_states), 3, 4))
332
+ hidden_states = hidden_states + gate(ff_out, 5)
333
+ return hidden_states
334
+
335
+ modulation = temb.unflatten(-1, (6, -1)) + self.scale_shift_table
336
+ prescale, preshift, pregate, postscale, postshift, postgate = modulation.unbind(-2)
337
+
338
+ attn_out = self.attn(
339
+ (1.0 + prescale) * self.norm1(hidden_states) + preshift,
340
+ attention_mask=attention_mask,
341
+ image_rotary_emb=image_rotary_emb,
342
+ )
343
+ hidden_states = hidden_states + pregate * attn_out
344
+ ff_out = self.ff((1.0 + postscale) * self.norm2(hidden_states) + postshift)
345
+ hidden_states = hidden_states + postgate * ff_out
346
+ return hidden_states
347
+
348
+
349
+ class Krea2TimestepEmbedding(nn.Module):
350
+ """Sinusoidal flow-time embedding (cos-first, input scaled by 1000) followed by a
351
+ two-layer MLP. Keeps the sequence dimension at size 1 so per-block modulations
352
+ broadcast over tokens."""
353
+
354
+ def __init__(self, embed_dim: int, hidden_size: int) -> None:
355
+ super().__init__()
356
+ self.embed_dim = embed_dim
357
+ self.linear_1 = nn.Linear(embed_dim, hidden_size, bias=True)
358
+ self.linear_2 = nn.Linear(hidden_size, hidden_size, bias=True)
359
+
360
+ def forward(self, timestep: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
361
+ half = self.embed_dim // 2
362
+ freqs = torch.exp(-math.log(1e4) * torch.arange(half, dtype=torch.float32, device=timestep.device) / half)
363
+ args = (timestep.float() * 1e3)[:, None, None] * freqs
364
+ emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1).to(dtype)
365
+ return self.linear_2(F.gelu(self.linear_1(emb), approximate="tanh"))
366
+
367
+
368
+ class Krea2TextProjection(nn.Module):
369
+ """Projects the fused text features into the transformer width."""
370
+
371
+ def __init__(self, text_dim: int, hidden_size: int, eps: float) -> None:
372
+ super().__init__()
373
+ self.norm = Krea2RMSNorm(text_dim, eps=eps)
374
+ self.linear_1 = nn.Linear(text_dim, hidden_size, bias=True)
375
+ self.linear_2 = nn.Linear(hidden_size, hidden_size, bias=True)
376
+
377
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
378
+ hidden_states = self.linear_1(self.norm(hidden_states))
379
+ return self.linear_2(F.gelu(hidden_states, approximate="tanh"))
380
+
381
+
382
+ class Krea2FinalLayer(nn.Module):
383
+ """Final adaptive RMSNorm and output projection."""
384
+
385
+ def __init__(self, hidden_size: int, out_channels: int, eps: float) -> None:
386
+ super().__init__()
387
+ self.scale_shift_table = nn.Parameter(torch.zeros(2, hidden_size))
388
+ self.norm = Krea2RMSNorm(hidden_size, eps=eps)
389
+ self.linear = nn.Linear(hidden_size, out_channels, bias=True)
390
+
391
+ def forward(self, hidden_states: torch.Tensor, temb: torch.Tensor) -> torch.Tensor:
392
+ modulation = temb + self.scale_shift_table
393
+ scale, shift = modulation.chunk(2, dim=1)
394
+ hidden_states = (1.0 + scale) * self.norm(hidden_states) + shift
395
+ return self.linear(hidden_states)
396
+
397
+
398
+ class Krea2Transformer2DModel(ModelMixin, ConfigMixin, PeftAdapterMixin):
399
+ r"""
400
+ The Krea 2 single-stream MMDiT flow-matching backbone, extended with support for
401
+ clean reference-image tokens ("edit" conditioning).
402
+
403
+ Text conditioning enters as a stack of hidden states tapped from several layers of
404
+ the Qwen3-VL text encoder. A small text-fusion transformer collapses the layer axis
405
+ and refines the token sequence; the result is concatenated with the patchified
406
+ image latents (and, optionally, packed reference latents) into a single
407
+ ``[text, image, refs]`` sequence processed by the transformer blocks.
408
+
409
+ When ``ref_seq_len > 0``, the last ``ref_seq_len`` tokens of ``hidden_states`` are
410
+ clean reference tokens: they are modulated with the t=0 timestep embedding
411
+ (Kontext-style "index_timestep_zero") and excluded from the returned velocity.
412
+ """
413
+
414
+ _supports_gradient_checkpointing = True
415
+ _no_split_modules = ["Krea2TransformerBlock", "Krea2TextFusionBlock", "Krea2FinalLayer"]
416
+ _keep_in_fp32_modules = ["norm", "norm1", "norm2", "norm_q", "norm_k"]
417
+ _skip_layerwise_casting_patterns = ["time_embed", "norm"]
418
+
419
+ @register_to_config
420
+ def __init__(
421
+ self,
422
+ in_channels: int = 64,
423
+ num_layers: int = 28,
424
+ attention_head_dim: int = 128,
425
+ num_attention_heads: int = 48,
426
+ num_key_value_heads: int = 12,
427
+ intermediate_size: int = 16384,
428
+ timestep_embed_dim: int = 256,
429
+ text_hidden_dim: int = 2560,
430
+ num_text_layers: int = 12,
431
+ text_num_attention_heads: int = 20,
432
+ text_num_key_value_heads: int = 20,
433
+ text_intermediate_size: int = 6912,
434
+ num_layerwise_text_blocks: int = 2,
435
+ num_refiner_text_blocks: int = 2,
436
+ axes_dims_rope: Tuple[int, int, int] = (32, 48, 48),
437
+ rope_theta: float = 1000.0,
438
+ norm_eps: float = 1e-5,
439
+ ) -> None:
440
+ super().__init__()
441
+
442
+ hidden_size = attention_head_dim * num_attention_heads
443
+ if sum(axes_dims_rope) != attention_head_dim:
444
+ raise ValueError(
445
+ f"sum(axes_dims_rope)={sum(axes_dims_rope)} must equal attention_head_dim={attention_head_dim}"
446
+ )
447
+
448
+ self.in_channels = in_channels
449
+ self.out_channels = in_channels
450
+ self.hidden_size = hidden_size
451
+ self.gradient_checkpointing = False
452
+
453
+ self.img_in = nn.Linear(in_channels, hidden_size, bias=True)
454
+ self.time_embed = Krea2TimestepEmbedding(timestep_embed_dim, hidden_size)
455
+ self.time_mod_proj = nn.Linear(hidden_size, 6 * hidden_size, bias=True)
456
+ self.text_fusion = Krea2TextFusion(
457
+ num_text_layers=num_text_layers,
458
+ dim=text_hidden_dim,
459
+ num_heads=text_num_attention_heads,
460
+ num_kv_heads=text_num_key_value_heads,
461
+ intermediate_size=text_intermediate_size,
462
+ num_layerwise_blocks=num_layerwise_text_blocks,
463
+ num_refiner_blocks=num_refiner_text_blocks,
464
+ eps=norm_eps,
465
+ )
466
+ self.txt_in = Krea2TextProjection(text_hidden_dim, hidden_size, eps=norm_eps)
467
+ self.rotary_emb = Krea2RotaryPosEmbed(theta=rope_theta, axes_dim=list(axes_dims_rope))
468
+
469
+ self.transformer_blocks = nn.ModuleList(
470
+ [
471
+ Krea2TransformerBlock(
472
+ hidden_size=hidden_size,
473
+ intermediate_size=intermediate_size,
474
+ num_heads=num_attention_heads,
475
+ num_kv_heads=num_key_value_heads,
476
+ norm_eps=norm_eps,
477
+ )
478
+ for _ in range(num_layers)
479
+ ]
480
+ )
481
+
482
+ self.final_layer = Krea2FinalLayer(hidden_size, out_channels=in_channels, eps=norm_eps)
483
+
484
+ def forward(
485
+ self,
486
+ hidden_states: torch.Tensor,
487
+ encoder_hidden_states: torch.Tensor,
488
+ timestep: torch.Tensor,
489
+ position_ids: torch.Tensor,
490
+ encoder_attention_mask: Optional[torch.Tensor] = None,
491
+ ref_seq_len: int = 0,
492
+ attention_kwargs: Optional[Dict[str, Any]] = None,
493
+ return_dict: bool = True,
494
+ ) -> Union[Transformer2DModelOutput, Tuple[torch.Tensor]]:
495
+ r"""
496
+ Predict the flow-matching velocity for the (noisy) image tokens.
497
+
498
+ Args:
499
+ hidden_states (`torch.Tensor` of shape `(batch_size, image_seq_len + ref_seq_len, in_channels)`):
500
+ Packed (patchified) noisy image latents, with any packed clean reference
501
+ latents appended at the end.
502
+ encoder_hidden_states (`torch.Tensor` of shape `(batch_size, text_seq_len, num_text_layers, text_hidden_dim)`):
503
+ Stack of tapped text-encoder hidden states per token.
504
+ timestep (`torch.Tensor` of shape `(batch_size,)`):
505
+ Flow-matching time in `[0, 1]` (1 is pure noise, 0 is clean data).
506
+ position_ids (`torch.Tensor` of shape `(text_seq_len + image_seq_len + ref_seq_len, 3)`):
507
+ `(t, h, w)` rotary coordinates for the combined sequence. Text rows are
508
+ all-zero; image rows hold the latent-grid coordinates; the i-th
509
+ reference image sits on frame axis `i + 1` with its own grid.
510
+ encoder_attention_mask (`torch.Tensor` of shape `(batch_size, text_seq_len)`, *optional*):
511
+ Boolean mask marking valid text tokens.
512
+ ref_seq_len (`int`, defaults to 0):
513
+ Number of trailing reference tokens in `hidden_states`. They receive the
514
+ t=0 modulation and are excluded from the output.
515
+ attention_kwargs (`dict`, *optional*):
516
+ When it contains a `scale` entry, sets the LoRA scale applied to this
517
+ transformer's adapters for the duration of the forward pass.
518
+
519
+ Returns:
520
+ The velocity tensor of shape `(batch_size, image_seq_len, in_channels)`.
521
+ """
522
+ if position_ids.ndim != 2 or position_ids.shape[-1] != 3:
523
+ raise ValueError(f"`position_ids` must have shape (sequence_length, 3), got {tuple(position_ids.shape)}.")
524
+
525
+ lora_scale = 1.0
526
+ if attention_kwargs is not None:
527
+ attention_kwargs = attention_kwargs.copy()
528
+ lora_scale = attention_kwargs.pop("scale", 1.0)
529
+ if USE_PEFT_BACKEND and lora_scale != 1.0:
530
+ scale_lora_layers(self, lora_scale)
531
+
532
+ batch_size, image_seq_len, _ = hidden_states.shape # includes ref tokens
533
+ text_seq_len = encoder_hidden_states.shape[1]
534
+
535
+ temb = self.time_embed(timestep, dtype=hidden_states.dtype)
536
+ temb_mod = self.time_mod_proj(F.gelu(temb, approximate="tanh"))
537
+
538
+ block_temb = temb_mod
539
+ if ref_seq_len > 0:
540
+ # Clean reference tokens are conditioned at t=0; everything else keeps t.
541
+ temb_zero = self.time_embed(torch.zeros_like(timestep), dtype=hidden_states.dtype)
542
+ ref_temb_mod = self.time_mod_proj(F.gelu(temb_zero, approximate="tanh"))
543
+ block_temb = (temb_mod, ref_temb_mod, text_seq_len + image_seq_len - ref_seq_len)
544
+
545
+ # An all-True mask (no padded text tokens, e.g. any batch-of-1 call) is
546
+ # equivalent to no mask; passing None keeps SDPA on its fast, low-memory
547
+ # (flash) path instead of a mask-materializing fallback.
548
+ if encoder_attention_mask is not None and bool(encoder_attention_mask.all()):
549
+ encoder_attention_mask = None
550
+
551
+ text_attention_mask = None
552
+ attention_mask = None
553
+ if encoder_attention_mask is not None:
554
+ # Key-padding masks of shape (B, 1, 1, L): padded text tokens are excluded
555
+ # as attention keys everywhere; their own (garbage) lanes are never read
556
+ # back and are dropped at the output slice.
557
+ text_attention_mask = encoder_attention_mask[:, None, None, :]
558
+ image_mask = encoder_attention_mask.new_ones((batch_size, image_seq_len))
559
+ attention_mask = torch.cat([encoder_attention_mask, image_mask], dim=1)[:, None, None, :]
560
+
561
+ encoder_hidden_states = self.text_fusion(encoder_hidden_states, attention_mask=text_attention_mask)
562
+ encoder_hidden_states = self.txt_in(encoder_hidden_states)
563
+
564
+ hidden_states = self.img_in(hidden_states)
565
+ hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1)
566
+
567
+ image_rotary_emb = self.rotary_emb(position_ids)
568
+
569
+ for block in self.transformer_blocks:
570
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
571
+ ckpt_func = getattr(self, "_gradient_checkpointing_func", None)
572
+ if ckpt_func is None:
573
+ hidden_states = torch.utils.checkpoint.checkpoint(
574
+ block, hidden_states, block_temb, image_rotary_emb, attention_mask, use_reentrant=False
575
+ )
576
+ else:
577
+ hidden_states = ckpt_func(block, hidden_states, block_temb, image_rotary_emb, attention_mask)
578
+ else:
579
+ hidden_states = block(hidden_states, block_temb, image_rotary_emb, attention_mask)
580
+
581
+ hidden_states = hidden_states[:, text_seq_len : text_seq_len + image_seq_len - ref_seq_len]
582
+ output = self.final_layer(hidden_states, temb)
583
+
584
+ if USE_PEFT_BACKEND and lora_scale != 1.0:
585
+ unscale_lora_layers(self, lora_scale)
586
+
587
+ if not return_dict:
588
+ return (output,)
589
+ return Transformer2DModelOutput(sample=output)
590
+
591
+
592
+ # The Krea 2 hub repos' `model_index.json` points the `transformer` component at
593
+ # `["diffusers", "Krea2Transformer2DModel"]`. Registering the vendored class into the
594
+ # diffusers namespace lets `DiffusionPipeline.from_pretrained` resolve it on diffusers
595
+ # releases that don't ship Krea 2 yet, and guarantees the loaded transformer supports
596
+ # the reference-image forward pass this pipeline needs (the class is a numerically
597
+ # identical superset of the upstream one for text-to-image).
598
+ diffusers.Krea2Transformer2DModel = Krea2Transformer2DModel
599
+
600
+
601
+ # ---------------------------------------------------------------------------
602
+ # LoRA key conversion (Ostris AI-Toolkit / reference-trainer -> diffusers/PEFT)
603
+ # ---------------------------------------------------------------------------
604
+
605
+
606
+ def _convert_non_diffusers_krea2_lora_to_diffusers(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
607
+ """Map original `krea-ai/krea-2` module names onto `Krea2Transformer2DModel`.
608
+ Handles the `diffusion_model.` prefix (AI-Toolkit saves / ComfyUI) and the
609
+ `base_model.model.` prefix, as well as bare module names."""
610
+ state_dict = {
611
+ (k[len("base_model.model.") :] if k.startswith("base_model.model.") else k): v for k, v in state_dict.items()
612
+ }
613
+ state_dict = {
614
+ (k[len("diffusion_model.") :] if k.startswith("diffusion_model.") else k): v for k, v in state_dict.items()
615
+ }
616
+
617
+ attn_map = {"wq": "to_q", "wk": "to_k", "wv": "to_v", "wo": "to_out.0", "gate": "to_gate"}
618
+ ff_map = {"gate": "ff.gate", "up": "ff.up", "down": "ff.down"}
619
+ # The original model stores these standalone modules under abbreviated
620
+ # `nn.Sequential`-style names.
621
+ standalone_map = {
622
+ "first": "img_in",
623
+ "last.linear": "final_layer.linear",
624
+ "tmlp.0": "time_embed.linear_1",
625
+ "tmlp.2": "time_embed.linear_2",
626
+ "tproj.1": "time_mod_proj",
627
+ "txtmlp.1": "txt_in.linear_1",
628
+ "txtmlp.3": "txt_in.linear_2",
629
+ "txtfusion.projector": "text_fusion.projector",
630
+ }
631
+
632
+ def convert_module(module):
633
+ m = re.match(r"blocks\.(\d+)\.(attn|mlp)\.(\w+)$", module)
634
+ if m:
635
+ idx, kind, sub = m.groups()
636
+ if kind == "attn" and sub in attn_map:
637
+ return f"transformer_blocks.{idx}.attn.{attn_map[sub]}"
638
+ if kind == "mlp" and sub in ff_map:
639
+ return f"transformer_blocks.{idx}.{ff_map[sub]}"
640
+ return None
641
+ m = re.match(r"txtfusion\.(layerwise_blocks|refiner_blocks)\.(\d+)\.(attn|mlp)\.(\w+)$", module)
642
+ if m:
643
+ block, idx, kind, sub = m.groups()
644
+ if kind == "attn" and sub in attn_map:
645
+ return f"text_fusion.{block}.{idx}.attn.{attn_map[sub]}"
646
+ if kind == "mlp" and sub in ff_map:
647
+ return f"text_fusion.{block}.{idx}.{ff_map[sub]}"
648
+ return None
649
+ return standalone_map.get(module)
650
+
651
+ converted_state_dict = {}
652
+ for key in list(state_dict):
653
+ match = re.search(r"\.(?:lora_[AB])\.weight$", key)
654
+ if match is None:
655
+ continue
656
+ diffusers_module = convert_module(key[: match.start()])
657
+ if diffusers_module is None:
658
+ continue
659
+ converted_state_dict[f"transformer.{diffusers_module}{key[match.start() :]}"] = state_dict.pop(key)
660
+
661
+ if len(state_dict) > 0:
662
+ raise ValueError(f"Could not convert LoRA keys: {sorted(state_dict.keys())}")
663
+
664
+ return converted_state_dict
665
+
666
+
667
+ def _normalize_lora_state_dict(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
668
+ """Normalize a Krea 2 LoRA state dict to PEFT `lora_A`/`lora_B` naming and fold any
669
+ `.alpha` tensors into `lora_B` so the effective scale is preserved."""
670
+ state_dict = {
671
+ k.replace(".lora_down.weight", ".lora_A.weight").replace(".lora_up.weight", ".lora_B.weight"): v
672
+ for k, v in state_dict.items()
673
+ }
674
+ # PEFT assumes lora_alpha == rank (scale 1.0) when no alpha is given; fold any
675
+ # explicit alpha into lora_B instead of plumbing network_alphas through.
676
+ for alpha_key in [k for k in state_dict if k.endswith(".alpha")]:
677
+ base = alpha_key[: -len(".alpha")]
678
+ a_key, b_key = base + ".lora_A.weight", base + ".lora_B.weight"
679
+ alpha = float(state_dict.pop(alpha_key))
680
+ if a_key in state_dict and b_key in state_dict:
681
+ rank = state_dict[a_key].shape[0]
682
+ if alpha != rank:
683
+ state_dict[b_key] = state_dict[b_key] * (alpha / rank)
684
+ return state_dict
685
+
686
+
687
+ # ---------------------------------------------------------------------------
688
+ # Pipeline
689
+ # ---------------------------------------------------------------------------
690
+
691
+
692
+ @dataclass
693
+ class Krea2PipelineOutput(BaseOutput):
694
+ """Output class for the Krea 2 pipeline.
695
+
696
+ Args:
697
+ images (`list[PIL.Image.Image]` or `np.ndarray`):
698
+ List of `num_batches * num_images_per_prompt` denoised PIL images or a
699
+ numpy array of shape `(batch_size, height, width, num_channels)`.
700
+ """
701
+
702
+ images: Union[List[PIL.Image.Image], np.ndarray]
703
+
704
+
705
+ def calculate_shift(
706
+ image_seq_len,
707
+ base_seq_len: int = 256,
708
+ max_seq_len: int = 6400,
709
+ base_shift: float = 0.5,
710
+ max_shift: float = 1.15,
711
+ ):
712
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
713
+ b = base_shift - m * base_seq_len
714
+ mu = image_seq_len * m + b
715
+ return mu
716
+
717
+
718
+ class Krea2OstrisEditPipeline(DiffusionPipeline):
719
+ r"""
720
+ Krea 2 text-to-image / reference-image-edit pipeline with Ostris AI-Toolkit LoRA
721
+ loading. See the module docstring for usage.
722
+
723
+ Args:
724
+ scheduler ([`FlowMatchEulerDiscreteScheduler`]):
725
+ Euler flow-matching scheduler configured with the Krea 2 resolution-aware
726
+ exponential time shift.
727
+ vae ([`AutoencoderKLQwenImage`]):
728
+ The Qwen-Image VAE (f8, 16 latent channels).
729
+ text_encoder ([`~transformers.Qwen3VLModel`]):
730
+ Qwen3-VL, including its vision tower (used to embed reference images into
731
+ the prompt conditioning).
732
+ tokenizer ([`~transformers.AutoTokenizer`]):
733
+ The tokenizer paired with the text encoder.
734
+ transformer ([`Krea2Transformer2DModel`]):
735
+ The Krea 2 single-stream MMDiT.
736
+ text_encoder_select_layers (`tuple[int, ...]`, *optional*):
737
+ Indices into the text encoder's `hidden_states` tuple whose states are
738
+ stacked per token as the transformer's text conditioning.
739
+ is_distilled (`bool`, *optional*, defaults to `False`):
740
+ Whether the transformer is the few-step distilled (Turbo) checkpoint. When
741
+ `True`, a fixed timestep shift `mu=1.15` is used and the call defaults
742
+ change to `num_inference_steps=8, guidance_scale=0.0`.
743
+ patch_size (`int`, *optional*, defaults to 2):
744
+ Side length of the square patches the latents are packed into.
745
+ """
746
+
747
+ model_cpu_offload_seq = "text_encoder->transformer->vae"
748
+
749
+ # Default hub repo used to lazily build the Qwen3-VL processor that turns
750
+ # reference images into vision tokens (the Krea 2 repos ship only a tokenizer).
751
+ vl_processor_id = "Qwen/Qwen3-VL-4B-Instruct"
752
+
753
+ def __init__(
754
+ self,
755
+ scheduler: FlowMatchEulerDiscreteScheduler,
756
+ vae: AutoencoderKLQwenImage,
757
+ text_encoder: Qwen3VLModel,
758
+ tokenizer: AutoTokenizer,
759
+ transformer: Krea2Transformer2DModel,
760
+ text_encoder_select_layers: Optional[Union[Tuple[int, ...], List[int]]] = None,
761
+ is_distilled: bool = False,
762
+ patch_size: int = 2,
763
+ ):
764
+ super().__init__()
765
+
766
+ self.register_modules(
767
+ scheduler=scheduler,
768
+ vae=vae,
769
+ text_encoder=text_encoder,
770
+ tokenizer=tokenizer,
771
+ transformer=transformer,
772
+ )
773
+ if text_encoder_select_layers is None:
774
+ text_encoder_select_layers = (2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35)
775
+ self.register_to_config(text_encoder_select_layers=tuple(text_encoder_select_layers))
776
+ self.text_encoder_select_layers = tuple(text_encoder_select_layers)
777
+ self.register_to_config(is_distilled=is_distilled)
778
+ self.vae_scale_factor = 2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8
779
+ self.register_to_config(patch_size=patch_size)
780
+ self.patch_size = patch_size
781
+ self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * self.patch_size)
782
+
783
+ # Fixed instruction template wrapped around every prompt. The system prefix is
784
+ # fed through the encoder as context but its hidden states are sliced off.
785
+ self.prompt_template_encode_prefix = (
786
+ "<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, text, "
787
+ "spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n"
788
+ )
789
+ self.prompt_template_encode_suffix = "<|im_end|>\n<|im_start|>assistant\n"
790
+ self.prompt_template_encode_start_idx = 34
791
+
792
+ self._vl_processor = None
793
+
794
+ # ------------------------------------------------------------------
795
+ # Prompt encoding (Qwen3-VL; reference images embedded via vision tokens)
796
+ # ------------------------------------------------------------------
797
+ @property
798
+ def vl_processor(self):
799
+ """Qwen3-VL AutoProcessor, loaded lazily (only needed when reference images are
800
+ encoded into the prompt)."""
801
+ if self._vl_processor is None:
802
+ from transformers import AutoProcessor
803
+
804
+ self._vl_processor = AutoProcessor.from_pretrained(self.vl_processor_id)
805
+ return self._vl_processor
806
+
807
+ @staticmethod
808
+ def _to_chw_tensor(image) -> torch.Tensor:
809
+ """Convert a PIL image / numpy array / CHW tensor to a float CHW tensor in [0, 1]."""
810
+ if isinstance(image, torch.Tensor):
811
+ t = image.squeeze(0) if image.ndim == 4 else image
812
+ t = t.float()
813
+ if t.min() < 0: # assume [-1, 1]
814
+ t = (t + 1.0) / 2.0
815
+ return t.clamp(0, 1)
816
+ if isinstance(image, np.ndarray):
817
+ image = PIL.Image.fromarray(image)
818
+ image = image.convert("RGB")
819
+ arr = np.asarray(image).astype(np.float32) / 255.0
820
+ return torch.from_numpy(arr).permute(2, 0, 1)
821
+
822
+ def _prep_vl_images(self, images: List[torch.Tensor], max_pixels: int) -> List[torch.Tensor]:
823
+ """Resize reference images for the Qwen3-VL pass: aspect-preserving downscale
824
+ (never upscaled) to fit ``max_pixels`` total area. The MLLM only needs a coarse
825
+ view of the references; high-res detail flows through the VAE ref latents."""
826
+ prepped = []
827
+ for img in images:
828
+ h, w = img.shape[1], img.shape[2]
829
+ scale = min(1.0, math.sqrt(max_pixels / (h * w)))
830
+ nh, nw = max(round(h * scale), 28), max(round(w * scale), 28)
831
+ if (nh, nw) != (h, w):
832
+ img = (
833
+ F.interpolate(img.unsqueeze(0).float(), size=(nh, nw), mode="bicubic", antialias=True)
834
+ .squeeze(0)
835
+ .clamp(0, 1)
836
+ )
837
+ prepped.append(img.float())
838
+ return prepped
839
+
840
+ def _encode_single_prompt(
841
+ self,
842
+ prompt: str,
843
+ images: Optional[List[torch.Tensor]] = None,
844
+ max_sequence_length: int = 512,
845
+ device: Optional[torch.device] = None,
846
+ ) -> torch.Tensor:
847
+ """Encode one prompt (optionally with reference images embedded as vision
848
+ tokens) into stacked Qwen3-VL hidden states of shape `(seq_len, num_text_layers,
849
+ text_hidden_dim)` at natural (unpadded) length."""
850
+ device = device or self._execution_device
851
+ prefix_idx = self.prompt_template_encode_start_idx
852
+
853
+ # The suffix is tokenized separately so it lands after the prompt tokens.
854
+ suffix_inputs = self.tokenizer([self.prompt_template_encode_suffix], return_tensors="pt").to(device)
855
+ suffix_ids = suffix_inputs["input_ids"]
856
+ suffix_mask = suffix_inputs["attention_mask"].bool()
857
+
858
+ extra_inputs = {}
859
+ if images:
860
+ # Reference images ride in the user message ahead of the prompt via named
861
+ # vision placeholders; the processor expands each <|image_pad|> to the
862
+ # image's token grid.
863
+ image_prompt = "".join(
864
+ f"Picture {i + 1}: <|vision_start|><|image_pad|><|vision_end|>" for i in range(len(images))
865
+ )
866
+ text = self.prompt_template_encode_prefix + image_prompt + prompt
867
+ # No truncation here: the expanded image-pad runs must stay intact.
868
+ inputs = self.vl_processor(text=[text], images=list(images), return_tensors="pt", do_rescale=False).to(
869
+ device
870
+ )
871
+ for k, v in inputs.items():
872
+ if k in ("input_ids", "attention_mask"):
873
+ continue
874
+ if isinstance(v, torch.Tensor) and v.is_floating_point():
875
+ v = v.to(self.text_encoder.dtype)
876
+ extra_inputs[k] = v
877
+ else:
878
+ text = self.prompt_template_encode_prefix + prompt
879
+ inputs = self.tokenizer(
880
+ [text], truncation=True, max_length=max_sequence_length + prefix_idx, return_tensors="pt"
881
+ ).to(device)
882
+
883
+ input_ids = torch.cat([inputs["input_ids"], suffix_ids], dim=1)
884
+ attention_mask = torch.cat([inputs["attention_mask"].bool(), suffix_mask], dim=1)
885
+
886
+ # mm_token_type_ids (used for M-RoPE) must cover the appended suffix tokens
887
+ # too; they are plain text -> type 0.
888
+ if "mm_token_type_ids" in extra_inputs:
889
+ tt = extra_inputs["mm_token_type_ids"]
890
+ extra_inputs["mm_token_type_ids"] = torch.cat(
891
+ [tt, torch.zeros_like(suffix_ids, dtype=tt.dtype)], dim=1
892
+ )
893
+
894
+ outputs = self.text_encoder(
895
+ input_ids=input_ids,
896
+ attention_mask=attention_mask,
897
+ output_hidden_states=True,
898
+ **extra_inputs,
899
+ )
900
+
901
+ hidden_states = torch.stack([outputs.hidden_states[i] for i in self.text_encoder_select_layers], dim=2)
902
+ # Drop the system-prefix tokens; what remains is (image +) prompt + suffix.
903
+ return hidden_states[0, prefix_idx:]
904
+
905
+ def encode_prompt(
906
+ self,
907
+ prompt: Union[str, List[str]],
908
+ images: Optional[List[torch.Tensor]] = None,
909
+ num_images_per_prompt: int = 1,
910
+ max_sequence_length: int = 512,
911
+ device: Optional[torch.device] = None,
912
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
913
+ """Encode prompts (all sharing the same reference images, if any) and right-pad
914
+ them into a batch. Returns `(prompt_embeds, prompt_embeds_mask)` of shapes
915
+ `(B, L, num_text_layers, D)` and `(B, L)` (bool)."""
916
+ device = device or self._execution_device
917
+ prompt = [prompt] if isinstance(prompt, str) else prompt
918
+
919
+ features = [self._encode_single_prompt(p, images, max_sequence_length, device) for p in prompt]
920
+ max_len = max(f.shape[0] for f in features)
921
+ embeds = features[0].new_zeros(len(features), max_len, *features[0].shape[1:])
922
+ mask = torch.zeros(len(features), max_len, dtype=torch.bool, device=device)
923
+ for i, f in enumerate(features):
924
+ embeds[i, : f.shape[0]] = f
925
+ mask[i, : f.shape[0]] = True
926
+
927
+ embeds = embeds.repeat_interleave(num_images_per_prompt, dim=0)
928
+ mask = mask.repeat_interleave(num_images_per_prompt, dim=0)
929
+ return embeds, mask
930
+
931
+ # ------------------------------------------------------------------
932
+ # Latent packing helpers
933
+ # ------------------------------------------------------------------
934
+ def _pack_latents(self, latents: torch.Tensor) -> torch.Tensor:
935
+ """(B, C, H, W) latents -> (B, H/p * W/p, C * p * p) tokens."""
936
+ b, c, h, w = latents.shape
937
+ p = self.patch_size
938
+ latents = latents.view(b, c, h // p, p, w // p, p)
939
+ latents = latents.permute(0, 2, 4, 1, 3, 5)
940
+ return latents.reshape(b, (h // p) * (w // p), c * p * p)
941
+
942
+ def _unpack_latents(self, latents: torch.Tensor, height: int, width: int) -> torch.Tensor:
943
+ """(B, L, C * p * p) tokens -> (B, C, 1, H, W) latents (frame dim for the VAE)."""
944
+ batch_size, _, channels = latents.shape
945
+ p = self.patch_size
946
+ h = p * (int(height) // (self.vae_scale_factor * p))
947
+ w = p * (int(width) // (self.vae_scale_factor * p))
948
+ latents = latents.view(batch_size, h // p, w // p, channels // (p * p), p, p)
949
+ latents = latents.permute(0, 3, 1, 4, 2, 5)
950
+ return latents.reshape(batch_size, channels // (p * p), 1, h, w)
951
+
952
+ def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None):
953
+ if latents is not None:
954
+ return latents.to(device=device, dtype=dtype)
955
+ shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor)
956
+ if isinstance(generator, list) and len(generator) != batch_size:
957
+ raise ValueError(
958
+ f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
959
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
960
+ )
961
+ latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
962
+ return self._pack_latents(latents)
963
+
964
+ def _encode_reference_latents(
965
+ self,
966
+ images: List[torch.Tensor],
967
+ max_pixels: int,
968
+ generator: Optional[torch.Generator],
969
+ device: torch.device,
970
+ ) -> List[torch.Tensor]:
971
+ """Encode `[0, 1]` CHW reference images to normalized VAE latents, one `(C, h, w)`
972
+ tensor per image. Each image is downscaled (aspect-preserving, never upscaled) to
973
+ fit within `max_pixels`, then snapped so the latent grid is patchifiable."""
974
+ snap = self.vae_scale_factor * self.patch_size
975
+ vae_dtype = self.vae.dtype
976
+
977
+ latents_mean = torch.tensor(self.vae.config.latents_mean).view(1, self.vae.config.z_dim, 1, 1, 1)
978
+ latents_std = torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1)
979
+
980
+ ref_latents = []
981
+ for img in images:
982
+ img = img.unsqueeze(0).to(device, dtype=vae_dtype)
983
+ h, w = img.shape[2], img.shape[3]
984
+ if h * w > max_pixels:
985
+ ratio = h / w
986
+ new_h, new_w = math.sqrt(max_pixels * ratio), math.sqrt(max_pixels / ratio)
987
+ else:
988
+ new_h, new_w = float(h), float(w)
989
+ new_h = max(snap, int(round(new_h / snap)) * snap)
990
+ new_w = max(snap, int(round(new_w / snap)) * snap)
991
+ if (new_h, new_w) != (h, w):
992
+ img = F.interpolate(img.float(), size=(new_h, new_w), mode="bilinear").to(vae_dtype)
993
+
994
+ img = (img * 2.0 - 1.0).unsqueeze(2) # [0,1] -> [-1,1], add frame dim
995
+ latent = self.vae.encode(img).latent_dist.sample(generator)
996
+ latent = (latent - latents_mean.to(latent.device, latent.dtype)) / latents_std.to(
997
+ latent.device, latent.dtype
998
+ )
999
+ ref_latents.append(latent[:, :, 0][0]) # drop frame + batch dims -> (C, h, w)
1000
+ return ref_latents
1001
+
1002
+ def _pack_reference_latents(
1003
+ self, ref_latents: List[torch.Tensor], device: torch.device, dtype: torch.dtype
1004
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
1005
+ """Patchify reference latents into `(1, ref_seq_len, C * p * p)` tokens and build
1006
+ their `(ref_seq_len, 3)` rotary coordinates. The i-th reference sits on frame
1007
+ axis `i + 1` with its own y/x grid starting at 0 (Kontext "index" placement)."""
1008
+ p = self.patch_size
1009
+ tokens, position_ids = [], []
1010
+ for i, ref in enumerate(ref_latents):
1011
+ ref = ref.unsqueeze(0).to(device, dtype)
1012
+ tokens.append(self._pack_latents(ref))
1013
+ _, _, h, w = ref.shape
1014
+ ids = torch.zeros(h // p, w // p, 3, device=device)
1015
+ ids[..., 0] = i + 1
1016
+ ids[..., 1] = torch.arange(h // p, device=device)[:, None]
1017
+ ids[..., 2] = torch.arange(w // p, device=device)[None, :]
1018
+ position_ids.append(ids.reshape(-1, 3))
1019
+ return torch.cat(tokens, dim=1), torch.cat(position_ids, dim=0)
1020
+
1021
+ @staticmethod
1022
+ def prepare_position_ids(text_seq_len: int, grid_height: int, grid_width: int, device: torch.device):
1023
+ """Rotary coordinates for the `[text, image]` sequence: text tokens sit at the
1024
+ origin, image tokens carry their `(0, h, w)` latent-grid coordinates."""
1025
+ text_ids = torch.zeros(text_seq_len, 3, device=device)
1026
+ image_ids = torch.zeros(grid_height, grid_width, 3, device=device)
1027
+ image_ids[..., 1] = torch.arange(grid_height, device=device)[:, None]
1028
+ image_ids[..., 2] = torch.arange(grid_width, device=device)[None, :]
1029
+ image_ids = image_ids.reshape(grid_height * grid_width, 3)
1030
+ return torch.cat([text_ids, image_ids], dim=0)
1031
+
1032
+ # ------------------------------------------------------------------
1033
+ # LoRA loading (Ostris AI-Toolkit / ComfyUI / diffusers formats)
1034
+ # ------------------------------------------------------------------
1035
+ def load_lora_weights(
1036
+ self,
1037
+ pretrained_model_name_or_path_or_dict: Union[str, Dict[str, torch.Tensor]],
1038
+ weight_name: Optional[str] = None,
1039
+ adapter_name: str = "default",
1040
+ **kwargs,
1041
+ ):
1042
+ r"""
1043
+ Load a Krea 2 LoRA into the transformer.
1044
+
1045
+ Accepts a state dict, a local `.safetensors` file or directory, or a hub repo id
1046
+ (with `weight_name` selecting the file when the repo holds several). Handles
1047
+ Ostris AI-Toolkit / ComfyUI key layouts (`diffusion_model.blocks...` with
1048
+ `lora_A`/`lora_B` or `lora_down`/`lora_up`) as well as already-converted
1049
+ diffusers-format state dicts (`transformer.transformer_blocks...`).
1050
+ """
1051
+ if isinstance(pretrained_model_name_or_path_or_dict, dict):
1052
+ state_dict = dict(pretrained_model_name_or_path_or_dict)
1053
+ else:
1054
+ from safetensors.torch import load_file
1055
+
1056
+ path = str(pretrained_model_name_or_path_or_dict)
1057
+ if os.path.isfile(path):
1058
+ file_path = path
1059
+ elif os.path.isdir(path):
1060
+ if weight_name is None:
1061
+ candidates = [f for f in os.listdir(path) if f.endswith(".safetensors")]
1062
+ if len(candidates) != 1:
1063
+ raise ValueError(
1064
+ f"Could not pick a LoRA file in {path}: found {candidates}. Pass `weight_name`."
1065
+ )
1066
+ weight_name = candidates[0]
1067
+ file_path = os.path.join(path, weight_name)
1068
+ else:
1069
+ from huggingface_hub import hf_hub_download, list_repo_files
1070
+
1071
+ if weight_name is None:
1072
+ candidates = [
1073
+ f for f in list_repo_files(path, token=kwargs.get("token", None)) if f.endswith(".safetensors")
1074
+ ]
1075
+ if len(candidates) != 1:
1076
+ raise ValueError(
1077
+ f"Could not pick a LoRA file in hub repo {path}: found {candidates}. Pass `weight_name`."
1078
+ )
1079
+ weight_name = candidates[0]
1080
+ file_path = hf_hub_download(path, weight_name, token=kwargs.get("token", None))
1081
+ state_dict = load_file(file_path)
1082
+
1083
+ state_dict = _normalize_lora_state_dict(state_dict)
1084
+ if not any(k.startswith("transformer.") for k in state_dict):
1085
+ state_dict = _convert_non_diffusers_krea2_lora_to_diffusers(state_dict)
1086
+
1087
+ self.transformer.load_lora_adapter(state_dict, prefix="transformer", adapter_name=adapter_name)
1088
+
1089
+ def unload_lora_weights(self):
1090
+ """Remove all loaded LoRA adapters from the transformer."""
1091
+ transformer = self.transformer
1092
+ if hasattr(transformer, "unload_lora"):
1093
+ transformer.unload_lora()
1094
+ elif getattr(transformer, "peft_config", None):
1095
+ transformer.delete_adapters(list(transformer.peft_config.keys()))
1096
+
1097
+ def fuse_lora(self, lora_scale: float = 1.0, adapter_names: Optional[List[str]] = None, **kwargs):
1098
+ """Fuse the loaded LoRA weights into the transformer for adapter-free inference."""
1099
+ self.transformer.fuse_lora(lora_scale=lora_scale, adapter_names=adapter_names, **kwargs)
1100
+
1101
+ def unfuse_lora(self, **kwargs):
1102
+ self.transformer.unfuse_lora(**kwargs)
1103
+
1104
+ def set_adapters(self, adapter_names: Union[str, List[str]], weights: Optional[Union[float, List[float]]] = None):
1105
+ """Activate (and optionally weight) specific loaded LoRA adapters."""
1106
+ self.transformer.set_adapters(adapter_names, weights)
1107
+
1108
+ # ------------------------------------------------------------------
1109
+ # Generation
1110
+ # ------------------------------------------------------------------
1111
+ @property
1112
+ def guidance_scale(self):
1113
+ return self._guidance_scale
1114
+
1115
+ @property
1116
+ def do_classifier_free_guidance(self):
1117
+ return self._guidance_scale > 0
1118
+
1119
+ @torch.no_grad()
1120
+ def __call__(
1121
+ self,
1122
+ prompt: Union[str, List[str], None] = None,
1123
+ image: Union[PIL.Image.Image, np.ndarray, torch.Tensor, List, None] = None,
1124
+ negative_prompt: Union[str, List[str], None] = None,
1125
+ height: int = 1024,
1126
+ width: int = 1024,
1127
+ num_inference_steps: Optional[int] = None,
1128
+ sigmas: Optional[List[float]] = None,
1129
+ guidance_scale: Optional[float] = None,
1130
+ num_images_per_prompt: int = 1,
1131
+ generator: Union[torch.Generator, List[torch.Generator], None] = None,
1132
+ latents: Optional[torch.Tensor] = None,
1133
+ prompt_embeds: Optional[torch.Tensor] = None,
1134
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
1135
+ negative_prompt_embeds: Optional[torch.Tensor] = None,
1136
+ negative_prompt_embeds_mask: Optional[torch.Tensor] = None,
1137
+ reference_max_pixels: int = 1024 * 1024,
1138
+ vl_image_max_pixels: int = 384 * 384,
1139
+ encode_reference_in_prompt: bool = True,
1140
+ output_type: Optional[str] = "pil",
1141
+ return_dict: bool = True,
1142
+ attention_kwargs: Optional[Dict[str, Any]] = None,
1143
+ max_sequence_length: int = 512,
1144
+ ):
1145
+ r"""
1146
+ Generate images from a prompt, optionally conditioned on reference images.
1147
+
1148
+ Args:
1149
+ prompt (`str` or `list[str]`):
1150
+ The prompt(s) to guide generation. For edits, describe the change (e.g.
1151
+ "make the sky purple").
1152
+ image (`PIL.Image.Image`, `np.ndarray`, `torch.Tensor` or a list of them, *optional*):
1153
+ Reference image(s). They are encoded into the prompt conditioning via
1154
+ the Qwen3-VL vision tower and appended to the transformer sequence as
1155
+ clean VAE latents at t=0. References keep their own aspect ratio; the
1156
+ output size is set by `height`/`width` independently.
1157
+ negative_prompt (`str` or `list[str]`, *optional*):
1158
+ Prompt(s) not to guide generation; ignored when `guidance_scale <= 0`.
1159
+ height / width (`int`, defaults to 1024):
1160
+ Output size in pixels; rounded up to a multiple of 16 if needed.
1161
+ num_inference_steps (`int`, *optional*):
1162
+ Denoising steps. Defaults to 8 for a distilled (Turbo) checkpoint and 28
1163
+ otherwise.
1164
+ sigmas (`list[float]`, *optional*):
1165
+ Custom sigma grid for the scheduler.
1166
+ guidance_scale (`float`, *optional*):
1167
+ Krea 2 CFG convention: velocity is `cond + scale * (cond - uncond)` and
1168
+ guidance is enabled whenever `scale > 0` (equals standard CFG with scale
1169
+ `1 + scale`). Defaults to 0.0 for a distilled checkpoint and 4.5
1170
+ otherwise.
1171
+ num_images_per_prompt (`int`, defaults to 1):
1172
+ Number of images per prompt.
1173
+ generator (`torch.Generator` or `list[torch.Generator]`, *optional*):
1174
+ RNG for deterministic generation.
1175
+ latents (`torch.Tensor`, *optional*):
1176
+ Pre-generated packed noisy latents `(B, image_seq_len, in_channels)`.
1177
+ prompt_embeds / prompt_embeds_mask (`torch.Tensor`, *optional*):
1178
+ Pre-computed text conditioning `(B, L, num_text_layers, D)` and its
1179
+ bool mask `(B, L)`; skips prompt encoding when given.
1180
+ negative_prompt_embeds / negative_prompt_embeds_mask (`torch.Tensor`, *optional*):
1181
+ Same, for the negative prompt.
1182
+ reference_max_pixels (`int`, defaults to `1024 * 1024`):
1183
+ Pixel budget each reference image is downscaled to fit before VAE
1184
+ encoding (never upscaled).
1185
+ vl_image_max_pixels (`int`, defaults to `384 * 384`):
1186
+ Pixel budget for the (coarse) Qwen3-VL view of each reference image.
1187
+ encode_reference_in_prompt (`bool`, defaults to `True`):
1188
+ Whether reference images are also embedded into the text conditioning
1189
+ through the Qwen3-VL vision tower (matches AI-Toolkit edit training).
1190
+ output_type (`str`, defaults to `"pil"`):
1191
+ `"pil"`, `"np"`, `"pt"` or `"latent"`.
1192
+ return_dict (`bool`, defaults to `True`):
1193
+ Whether to return a [`Krea2PipelineOutput`] instead of a plain tuple.
1194
+ attention_kwargs (`dict`, *optional*):
1195
+ Forwarded to the transformer; a `scale` entry sets the LoRA scale.
1196
+ max_sequence_length (`int`, defaults to 512):
1197
+ Maximum prompt token length (truncation only; no fixed padding).
1198
+
1199
+ Returns:
1200
+ [`Krea2PipelineOutput`] or `tuple`: the generated images.
1201
+ """
1202
+ if num_inference_steps is None:
1203
+ num_inference_steps = 8 if self.config.is_distilled else 28
1204
+ if guidance_scale is None:
1205
+ guidance_scale = 0.0 if self.config.is_distilled else 4.5
1206
+
1207
+ multiple = self.vae_scale_factor * self.patch_size
1208
+ if height % multiple != 0 or width % multiple != 0:
1209
+ rounded_height = ((height + multiple - 1) // multiple) * multiple
1210
+ rounded_width = ((width + multiple - 1) // multiple) * multiple
1211
+ logger.warning(
1212
+ f"`height` and `width` must be multiples of {multiple}; rounding up from {height}x{width} to"
1213
+ f" {rounded_height}x{rounded_width}."
1214
+ )
1215
+ height, width = rounded_height, rounded_width
1216
+
1217
+ if prompt is None and prompt_embeds is None:
1218
+ raise ValueError("Provide either `prompt` or `prompt_embeds`.")
1219
+ if prompt_embeds is not None and prompt_embeds_mask is None:
1220
+ raise ValueError("`prompt_embeds` requires `prompt_embeds_mask`.")
1221
+ if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
1222
+ raise ValueError("`negative_prompt_embeds` requires `negative_prompt_embeds_mask`.")
1223
+
1224
+ self._guidance_scale = guidance_scale
1225
+
1226
+ if prompt is not None and isinstance(prompt, str):
1227
+ batch_size = 1
1228
+ elif prompt is not None:
1229
+ batch_size = len(prompt)
1230
+ else:
1231
+ batch_size = prompt_embeds.shape[0]
1232
+
1233
+ device = self._execution_device
1234
+ transformer_dtype = self.transformer.dtype
1235
+
1236
+ # 1. Normalize reference images to a list of [0, 1] CHW tensors.
1237
+ ref_images = None
1238
+ if image is not None:
1239
+ image_list = image if isinstance(image, (list, tuple)) else [image]
1240
+ ref_images = [self._to_chw_tensor(img) for img in image_list]
1241
+
1242
+ # 2. Encode the prompt(s). With references, the coarse VL view of each image is
1243
+ # embedded in the user message so the text conditioning "sees" them.
1244
+ vl_images = None
1245
+ if ref_images is not None and encode_reference_in_prompt:
1246
+ vl_images = self._prep_vl_images([img.to(device) for img in ref_images], vl_image_max_pixels)
1247
+
1248
+ if prompt_embeds is None:
1249
+ prompt_embeds, prompt_embeds_mask = self.encode_prompt(
1250
+ prompt, vl_images, num_images_per_prompt, max_sequence_length, device
1251
+ )
1252
+ prompt_embeds = prompt_embeds.to(transformer_dtype)
1253
+
1254
+ if self.do_classifier_free_guidance:
1255
+ if negative_prompt_embeds is None:
1256
+ negative_prompt = negative_prompt if negative_prompt is not None else ""
1257
+ if isinstance(negative_prompt, str):
1258
+ negative_prompt = [negative_prompt] * batch_size
1259
+ negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
1260
+ negative_prompt, vl_images, num_images_per_prompt, max_sequence_length, device
1261
+ )
1262
+ negative_prompt_embeds = negative_prompt_embeds.to(transformer_dtype)
1263
+
1264
+ # 3. Prepare the noisy latents (kept in float32 across scheduler steps).
1265
+ num_channels_latents = self.transformer.config.in_channels // (self.patch_size**2)
1266
+ latents = self.prepare_latents(
1267
+ batch_size * num_images_per_prompt,
1268
+ num_channels_latents,
1269
+ height,
1270
+ width,
1271
+ torch.float32,
1272
+ device,
1273
+ generator,
1274
+ latents,
1275
+ )
1276
+ grid_height = height // (self.vae_scale_factor * self.patch_size)
1277
+ grid_width = width // (self.vae_scale_factor * self.patch_size)
1278
+
1279
+ # 4. Encode + pack reference latents (shared across the batch) and build the
1280
+ # combined rotary coordinates.
1281
+ ref_tokens, ref_seq_len = None, 0
1282
+ neg_position_ids = None
1283
+ position_ids = self.prepare_position_ids(prompt_embeds.shape[1], grid_height, grid_width, device)
1284
+ if self.do_classifier_free_guidance:
1285
+ neg_position_ids = self.prepare_position_ids(
1286
+ negative_prompt_embeds.shape[1], grid_height, grid_width, device
1287
+ )
1288
+ if ref_images is not None:
1289
+ ref_latents = self._encode_reference_latents(ref_images, reference_max_pixels, generator, device)
1290
+ ref_tokens, ref_position_ids = self._pack_reference_latents(ref_latents, device, transformer_dtype)
1291
+ ref_seq_len = ref_tokens.shape[1]
1292
+ ref_tokens = ref_tokens.expand(latents.shape[0], -1, -1)
1293
+ position_ids = torch.cat([position_ids, ref_position_ids], dim=0)
1294
+ if neg_position_ids is not None:
1295
+ neg_position_ids = torch.cat([neg_position_ids, ref_position_ids], dim=0)
1296
+
1297
+ # 5. Prepare timesteps. The distilled (Turbo) checkpoint was trained at a fixed
1298
+ # exponential time shift mu=1.15; the base checkpoint interpolates mu from the
1299
+ # image token count.
1300
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
1301
+ if self.config.is_distilled:
1302
+ mu = 1.15
1303
+ else:
1304
+ mu = calculate_shift(
1305
+ grid_height * grid_width,
1306
+ self.scheduler.config.get("base_image_seq_len", 256),
1307
+ self.scheduler.config.get("max_image_seq_len", 6400),
1308
+ self.scheduler.config.get("base_shift", 0.5),
1309
+ self.scheduler.config.get("max_shift", 1.15),
1310
+ )
1311
+ self.scheduler.set_timesteps(sigmas=sigmas, device=device, mu=mu)
1312
+ timesteps = self.scheduler.timesteps
1313
+ self.scheduler.set_begin_index(0)
1314
+
1315
+ # 6. Denoising loop (Euler flow ODE integration via the scheduler).
1316
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
1317
+ for t in timesteps:
1318
+ timestep = (t / self.scheduler.config.num_train_timesteps).expand(latents.shape[0]).to(
1319
+ transformer_dtype
1320
+ )
1321
+
1322
+ model_input = latents.to(transformer_dtype)
1323
+ if ref_tokens is not None:
1324
+ model_input = torch.cat([model_input, ref_tokens], dim=1)
1325
+
1326
+ noise_pred = self.transformer(
1327
+ hidden_states=model_input,
1328
+ encoder_hidden_states=prompt_embeds,
1329
+ timestep=timestep,
1330
+ position_ids=position_ids,
1331
+ encoder_attention_mask=prompt_embeds_mask,
1332
+ ref_seq_len=ref_seq_len,
1333
+ attention_kwargs=attention_kwargs,
1334
+ return_dict=False,
1335
+ )[0]
1336
+
1337
+ if self.do_classifier_free_guidance:
1338
+ neg_noise_pred = self.transformer(
1339
+ hidden_states=model_input,
1340
+ encoder_hidden_states=negative_prompt_embeds,
1341
+ timestep=timestep,
1342
+ position_ids=neg_position_ids,
1343
+ encoder_attention_mask=negative_prompt_embeds_mask,
1344
+ ref_seq_len=ref_seq_len,
1345
+ attention_kwargs=attention_kwargs,
1346
+ return_dict=False,
1347
+ )[0]
1348
+ noise_pred = noise_pred + guidance_scale * (noise_pred - neg_noise_pred)
1349
+
1350
+ latents = self.scheduler.step(noise_pred.float(), t, latents, return_dict=False)[0]
1351
+ progress_bar.update()
1352
+
1353
+ # 7. Decode latents.
1354
+ if output_type == "latent":
1355
+ image_out = latents
1356
+ else:
1357
+ latents = self._unpack_latents(latents, height, width).to(self.vae.dtype)
1358
+ latents_mean = (
1359
+ torch.tensor(self.vae.config.latents_mean)
1360
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
1361
+ .to(latents.device, latents.dtype)
1362
+ )
1363
+ latents_std = (
1364
+ torch.tensor(self.vae.config.latents_std)
1365
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
1366
+ .to(latents.device, latents.dtype)
1367
+ )
1368
+ latents = latents * latents_std + latents_mean
1369
+ image_out = self.vae.decode(latents, return_dict=False)[0][:, :, 0]
1370
+ image_out = self.image_processor.postprocess(image_out, output_type=output_type)
1371
+
1372
+ self.maybe_free_model_hooks()
1373
+
1374
+ if not return_dict:
1375
+ return (image_out,)
1376
+ return Krea2PipelineOutput(images=image_out)