OzzyGT HF Staff commited on
Commit
3a6b18a
·
1 Parent(s): f91cae8

krea-2 reference blocks

Browse files
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__/
2
+ .ruff_cache/
README.md CHANGED
@@ -1,3 +1,13 @@
1
  ---
 
 
 
 
 
 
 
 
 
 
2
  license: apache-2.0
3
  ---
 
1
  ---
2
+ library_name: diffusers
3
+ pipeline_tag: text-to-image
4
+ base_model: krea/Krea-2-Raw
5
+ tags:
6
+ - text-to-image
7
+ - image-to-image
8
+ - reference-image
9
+ - modular-diffusers
10
+ - diffusion
11
+ - krea
12
  license: apache-2.0
13
  ---
__init__.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .krea2_reference import (
2
+ DEFAULT_GROUNDING_PX,
3
+ REFERENCE_MASK_MODE_DEEMPHASIZE,
4
+ REFERENCE_MASK_MODE_EXCLUDE,
5
+ REFERENCE_MASK_MODE_EXCLUDE_BLANK,
6
+ REFERENCE_MASK_MODES,
7
+ REFERENCE_MODE_APPEND,
8
+ REFERENCE_MODE_OFF,
9
+ REFERENCE_MODE_PREPEND,
10
+ REFERENCE_MODES,
11
+ Krea2AppendReferenceImagesStep,
12
+ Krea2AppendReferenceTextEncoderStep,
13
+ Krea2AppendReferenceWorkflow,
14
+ Krea2PrependReferenceImagesStep,
15
+ Krea2PrependReferenceTextEncoderStep,
16
+ Krea2PrependReferenceWorkflow,
17
+ Krea2ReferenceAutoBlocks,
18
+ Krea2ReferenceLatentsStep,
19
+ Krea2ReferenceTextInputsStep,
20
+ Krea2TurboAppendReferenceWorkflow,
21
+ Krea2TurboPrependReferenceWorkflow,
22
+ Krea2TurboReferenceAutoBlocks,
23
+ Krea2TurboVisionReferenceWorkflow,
24
+ Krea2VisionReferenceImagesStep,
25
+ Krea2VisionReferenceTextEncoderStep,
26
+ Krea2VisionReferenceWorkflow,
27
+ )
28
+
29
+
30
+ __all__ = [
31
+ # Top level: `reference_mode` selects the workflow.
32
+ "Krea2ReferenceAutoBlocks",
33
+ "Krea2TurboReferenceAutoBlocks",
34
+ # Per-mode workflows.
35
+ "Krea2VisionReferenceWorkflow",
36
+ "Krea2AppendReferenceWorkflow",
37
+ "Krea2PrependReferenceWorkflow",
38
+ "Krea2TurboVisionReferenceWorkflow",
39
+ "Krea2TurboAppendReferenceWorkflow",
40
+ "Krea2TurboPrependReferenceWorkflow",
41
+ # Leaf blocks.
42
+ "Krea2VisionReferenceImagesStep",
43
+ "Krea2AppendReferenceImagesStep",
44
+ "Krea2PrependReferenceImagesStep",
45
+ "Krea2VisionReferenceTextEncoderStep",
46
+ "Krea2AppendReferenceTextEncoderStep",
47
+ "Krea2PrependReferenceTextEncoderStep",
48
+ "Krea2ReferenceTextInputsStep",
49
+ "Krea2ReferenceLatentsStep",
50
+ # Mode constants.
51
+ "DEFAULT_GROUNDING_PX",
52
+ "REFERENCE_MODES",
53
+ "REFERENCE_MODE_APPEND",
54
+ "REFERENCE_MODE_OFF",
55
+ "REFERENCE_MODE_PREPEND",
56
+ "REFERENCE_MASK_MODES",
57
+ "REFERENCE_MASK_MODE_DEEMPHASIZE",
58
+ "REFERENCE_MASK_MODE_EXCLUDE",
59
+ "REFERENCE_MASK_MODE_EXCLUDE_BLANK",
60
+ ]
krea2_reference.py ADDED
@@ -0,0 +1,1790 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Custom modular-diffusers blocks adding reference-image conditioning to Krea 2.
15
+
16
+ Krea 2's text encoder is the full Qwen3-VL VLM, vision tower included, so reference images can condition
17
+ generation through the encoder's vision path. Two community edit LoRAs also feed the references as *clean VAE
18
+ latents* inside the transformer sequence. All three paths live here, selected by `reference_mode`:
19
+
20
+ * `"off"` (default) -- vision path only. Works on the stock base checkpoint, no LoRA.
21
+ * `"append"` -- vision (coarse view) + clean VAE reference tokens **after** the noisy target, modulated at flow
22
+ time `t=0`. The Ostris AI-Toolkit reference/edit LoRA recipe.
23
+ * `"prepend"` -- vision (grounded view) + the clean VAE source **before** the noisy target under the plain
24
+ uniform timestep modulation, distinguished only by its RoPE frame axis. The Identity-Edit recipe
25
+ (ai-toolkit `predict_velocity_edit`); its unconditional branch is grounded on the same image.
26
+
27
+ With `reference_mode="off"` and no images the blocks reduce to stock text-to-image. No mode patches the
28
+ `Krea2Transformer2DModel` source.
29
+ """
30
+
31
+ import math
32
+ from typing import Any
33
+
34
+ import numpy as np
35
+ import PIL.Image
36
+ import torch
37
+ import torch.nn.functional as F
38
+ from transformers import Qwen3VLProcessor
39
+
40
+ from diffusers.configuration_utils import FrozenDict
41
+ from diffusers.guiders import ClassifierFreeGuidance
42
+ from diffusers.models import AutoencoderKLQwenImage
43
+ from diffusers.models.transformers.transformer_krea2 import Krea2Transformer2DModel
44
+ from diffusers.modular_pipelines.krea2.before_denoise import (
45
+ Krea2PrepareLatentsStep,
46
+ Krea2PreparePositionIdsStep,
47
+ Krea2SetTimestepsStep,
48
+ Krea2TurboSetTimestepsStep,
49
+ )
50
+ from diffusers.modular_pipelines.krea2.decoders import Krea2DecodeStep
51
+ from diffusers.modular_pipelines.krea2.denoise import (
52
+ Krea2DenoiseLoopWrapper,
53
+ Krea2LoopAfterDenoiser,
54
+ Krea2LoopBeforeDenoiser,
55
+ )
56
+ from diffusers.modular_pipelines.krea2.encoders import (
57
+ _PROMPT_TEMPLATE_ENCODE_PREFIX,
58
+ _PROMPT_TEMPLATE_ENCODE_START_IDX,
59
+ _PROMPT_TEMPLATE_ENCODE_SUFFIX,
60
+ KREA2_TEXT_ENCODER_SELECT_LAYERS,
61
+ Krea2TextEncoderStep,
62
+ )
63
+ from diffusers.modular_pipelines.krea2.modular_pipeline import Krea2ModularPipeline
64
+ from diffusers.modular_pipelines.modular_pipeline import (
65
+ BlockState,
66
+ ConditionalPipelineBlocks,
67
+ ModularPipelineBlocks,
68
+ PipelineState,
69
+ SequentialPipelineBlocks,
70
+ )
71
+ from diffusers.modular_pipelines.modular_pipeline_utils import (
72
+ ComponentSpec,
73
+ InputParam,
74
+ InsertableDict,
75
+ OutputParam,
76
+ )
77
+ from diffusers.utils import logging
78
+
79
+ from .krea2_vision_attention import build_patch_keeps, vision_key_masks
80
+
81
+
82
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
83
+
84
+
85
+ REFERENCE_MODE_OFF = "off"
86
+ REFERENCE_MODE_APPEND = "append"
87
+ REFERENCE_MODE_PREPEND = "prepend"
88
+ REFERENCE_MODES = (REFERENCE_MODE_OFF, REFERENCE_MODE_APPEND, REFERENCE_MODE_PREPEND)
89
+
90
+ # What a painted mask does to the Qwen3-VL vision path. Zeroing output tokens alone cannot remove content
91
+ # from a reference (see `krea2_vision_attention`), so `deemphasize` attenuates rather than excludes.
92
+ REFERENCE_MASK_MODE_EXCLUDE_BLANK = "exclude_blank"
93
+ REFERENCE_MASK_MODE_EXCLUDE = "exclude"
94
+ REFERENCE_MASK_MODE_DEEMPHASIZE = "deemphasize"
95
+ REFERENCE_MASK_MODES = (
96
+ REFERENCE_MASK_MODE_EXCLUDE_BLANK,
97
+ REFERENCE_MASK_MODE_EXCLUDE,
98
+ REFERENCE_MASK_MODE_DEEMPHASIZE,
99
+ )
100
+
101
+ # Qwen3-VL vision-path budgets. `off` matches the reference ComfyUI encoder's default; `append` matches the
102
+ # AI-Toolkit recipe. `prepend` sizes its view from `grounding_px` instead.
103
+ VISION_MAX_PIXELS = 1024 * 1024
104
+ VISION_EDIT_MAX_PIXELS = 384 * 384
105
+
106
+ REFERENCE_LATENTS_MAX_PIXELS = 1024 * 1024
107
+
108
+ # Longest-side cap (px) for the `prepend` grounding view; 0 means native. The Identity-Edit LoRA trained on
109
+ # 384-768px jitter.
110
+ DEFAULT_GROUNDING_PX = 768
111
+
112
+
113
+ def _coerce_reference_mode(value: Any) -> str:
114
+ """Normalize a reference mode, falling back to `"off"` for anything unrecognized."""
115
+ if isinstance(value, str) and value in REFERENCE_MODES:
116
+ return value
117
+ if value is not None and value != REFERENCE_MODE_OFF:
118
+ logger.warning(f"Unknown `reference_mode` {value!r}; falling back to {REFERENCE_MODE_OFF!r}.")
119
+ return REFERENCE_MODE_OFF
120
+
121
+
122
+ def _coerce_reference_mask_mode(value: Any) -> str:
123
+ """Normalize a reference-mask mode, falling back to `"exclude_blank"` for anything unrecognized."""
124
+ if isinstance(value, str) and value in REFERENCE_MASK_MODES:
125
+ return value
126
+ if value is not None:
127
+ logger.warning(
128
+ f"Unknown `reference_mask_mode` {value!r}; falling back to {REFERENCE_MASK_MODE_EXCLUDE_BLANK!r}."
129
+ )
130
+ return REFERENCE_MASK_MODE_EXCLUDE_BLANK
131
+
132
+
133
+ def _as_list(value: Any) -> list:
134
+ """Wrap a bare value into a single-element list; `None` becomes an empty list."""
135
+ if value is None:
136
+ return []
137
+ if isinstance(value, (list, tuple)):
138
+ return list(value)
139
+ return [value]
140
+
141
+
142
+ def _broadcast_per_image(value: Any, num_images: int, default: float) -> list[float]:
143
+ """Expand a scalar (or short list) of per-image strengths to one float per reference image."""
144
+ values = _as_list(value)
145
+ if not values:
146
+ return [default] * num_images
147
+ if len(values) == 1:
148
+ return [float(values[0])] * num_images
149
+ values = [float(v) for v in values[:num_images]]
150
+ return values + [default] * (num_images - len(values))
151
+
152
+
153
+ def _to_rgb(image) -> PIL.Image.Image:
154
+ if not isinstance(image, PIL.Image.Image):
155
+ image = PIL.Image.fromarray(np.asarray(image))
156
+ return image.convert("RGB")
157
+
158
+
159
+ def _cap_area(image, max_pixels: int) -> PIL.Image.Image:
160
+ """Aspect-preserving downscale so the image covers at most `max_pixels`. Never upscales."""
161
+ image = _to_rgb(image)
162
+ pixels = image.width * image.height
163
+ if pixels > max_pixels:
164
+ scale = math.sqrt(max_pixels / pixels)
165
+ image = image.resize((max(1, round(image.width * scale)), max(1, round(image.height * scale))))
166
+ return image
167
+
168
+
169
+ def _cap_longest_side(image, longest_side: int) -> PIL.Image.Image:
170
+ """Downscale so the longest side is at most `longest_side` px (0 = native). Never upscales."""
171
+ image = _to_rgb(image)
172
+ longest = max(image.width, image.height)
173
+ if longest_side and longest > longest_side:
174
+ scale = longest_side / longest
175
+ image = image.resize((max(1, round(image.width * scale)), max(1, round(image.height * scale))))
176
+ return image
177
+
178
+
179
+ def _mask_to_keep_grid(mask, grid_height: int, grid_width: int):
180
+ """Rasterize a painted mask onto a `(grid_height, grid_width)` bool grid, `True` = keep.
181
+
182
+ Painted is the part to *use*. Shared by every consumer so the polarity means the same thing on the
183
+ pre-merge patch grid, the merged vision-token grid and the VAE latent patch grid. `RGBA` masks are read
184
+ from their alpha channel, anything else from luminance. Returns `None` when the mask is absent or paints
185
+ nothing -- an empty mask read literally would drop every token and silently disable the reference.
186
+ """
187
+ if mask is None or grid_height <= 0 or grid_width <= 0:
188
+ return None
189
+ if not isinstance(mask, PIL.Image.Image):
190
+ mask = PIL.Image.fromarray(np.asarray(mask))
191
+ painted = np.array(mask)[:, :, 3] if mask.mode == "RGBA" else np.array(mask.convert("L"))
192
+ painted = PIL.Image.fromarray(painted.astype(np.uint8)).resize((grid_width, grid_height), PIL.Image.BILINEAR)
193
+
194
+ keep = np.asarray(painted, dtype=np.float32) / 255.0 >= 0.5
195
+ if not keep.any():
196
+ logger.warning(
197
+ "A reference mask paints nothing and is being ignored. `RGBA` masks are read from their alpha "
198
+ "channel, so loading one through a helper that flattens to RGB (such as `load_image`) discards it."
199
+ )
200
+ return None
201
+ return keep
202
+
203
+
204
+ def _mask_to_keep_vector(mask, grid_height: int, grid_width: int, temporal: int = 1):
205
+ """Row-major flattened `_mask_to_keep_grid`, tiled over `temporal` frames.
206
+
207
+ Row-major is correct at merged-token resolution; the pre-merge patch sequence is not row-major and uses
208
+ `krea2_vision_attention.patch_keep_vector` instead.
209
+ """
210
+ keep = _mask_to_keep_grid(mask, grid_height, grid_width)
211
+ if keep is None:
212
+ return None
213
+ flat = keep.astype(np.float32).reshape(-1)
214
+ if temporal > 1:
215
+ flat = np.tile(flat, temporal)
216
+ return torch.from_numpy(flat)
217
+
218
+
219
+ def _build_keep_vectors(masks: list, image_grid_thw, merge_size: int) -> list:
220
+ """Build one keep vector per row of `image_grid_thw` (or `None` where the slot has no effective mask)."""
221
+ if image_grid_thw is None:
222
+ return []
223
+ keep_vectors = []
224
+ for i in range(int(image_grid_thw.shape[0])):
225
+ mask = masks[i] if i < len(masks) else None
226
+ t, h, w = (int(x) for x in image_grid_thw[i].tolist())
227
+ keep_vectors.append(_mask_to_keep_vector(mask, h // merge_size, w // merge_size, t))
228
+ return keep_vectors
229
+
230
+
231
+ def _scaled_get_image_features(original_fn, style_strengths, subject_strengths, merge_size, keep_vectors=None):
232
+ """Wrap `Qwen3VLModel.get_image_features` to scale each reference's vision features per channel.
233
+
234
+ Qwen3-VL injects each image twice: as scattered `<|image_pad|>` embeddings (`pooler_output`, a per-image
235
+ list) and as deepstack features (per-layer tensors re-injected at deeper LLM layers). `subject` scales the
236
+ former, `style` the latter; 0 removes that channel, 1.0 leaves it at full effect.
237
+
238
+ `keep_vectors` (one entry per image, or `None`) additionally zeroes the vision tokens outside the painted
239
+ region in both channels.
240
+ """
241
+
242
+ def strength_for(values, index):
243
+ return float(values[index]) if index < len(values) else 1.0
244
+
245
+ def wrapper(pixel_values, image_grid_thw, **kwargs):
246
+ outputs = original_fn(pixel_values, image_grid_thw, **kwargs)
247
+
248
+ def keep_for(index, like):
249
+ if not keep_vectors or index >= len(keep_vectors) or keep_vectors[index] is None:
250
+ return None
251
+ return keep_vectors[index].to(device=like.device, dtype=like.dtype)
252
+
253
+ pooled = getattr(outputs, "pooler_output", None)
254
+ if isinstance(pooled, (list, tuple)):
255
+ scaled = []
256
+ for i, features in enumerate(pooled):
257
+ features = features * strength_for(subject_strengths, i)
258
+ keep = keep_for(i, features)
259
+ if keep is not None:
260
+ features = features * keep.unsqueeze(-1)
261
+ scaled.append(features)
262
+ outputs.pooler_output = type(pooled)(scaled)
263
+
264
+ deepstack = getattr(outputs, "deepstack_features", None)
265
+ if deepstack:
266
+ reference = deepstack[0]
267
+ counts = [
268
+ int(image_grid_thw[i].prod().item()) // (merge_size * merge_size)
269
+ for i in range(image_grid_thw.shape[0])
270
+ ]
271
+ blocks = []
272
+ for i, count in enumerate(counts):
273
+ block = torch.full(
274
+ (count,), strength_for(style_strengths, i), device=reference.device, dtype=reference.dtype
275
+ )
276
+ keep = keep_for(i, reference)
277
+ if keep is not None:
278
+ block = block * keep
279
+ blocks.append(block)
280
+ per_token = torch.cat(blocks).unsqueeze(-1)
281
+ outputs.deepstack_features = type(deepstack)(layer * per_token for layer in deepstack)
282
+
283
+ return outputs
284
+
285
+ return wrapper
286
+
287
+
288
+ def _build_reference_processor(text_encoder, tokenizer):
289
+ """Build a `Qwen3VLProcessor` for reference-image conditioning.
290
+
291
+ Krea 2 ships no image-processor config, so one is derived here. Qwen3-VL reuses the Qwen2-VL image
292
+ processor; the video processor is unused but the processor class requires a non-`None` instance. The
293
+ patch/merge/temporal sizes MUST come from the model's own vision config (Qwen3-VL uses `patch_size=16`, not
294
+ the Qwen2-VL default of 14) or the patch-embed reshape fails.
295
+ """
296
+ from transformers import Qwen3VLProcessor, Qwen3VLVideoProcessor
297
+ from transformers.models.qwen2_vl.image_processing_qwen2_vl_fast import Qwen2VLImageProcessorFast
298
+
299
+ vision_config = text_encoder.config.vision_config
300
+ image_processor = Qwen2VLImageProcessorFast(
301
+ patch_size=vision_config.patch_size,
302
+ merge_size=vision_config.spatial_merge_size,
303
+ temporal_patch_size=vision_config.temporal_patch_size,
304
+ )
305
+ return Qwen3VLProcessor(
306
+ image_processor=image_processor, tokenizer=tokenizer, video_processor=Qwen3VLVideoProcessor()
307
+ )
308
+
309
+
310
+ def _pack_latents(latents: torch.Tensor, patch_size: int) -> torch.Tensor:
311
+ """Pack spatial latents `(B, C, H, W)` into Krea 2's sequence `(B, (H/p) * (W/p), C * p * p)`."""
312
+ batch_size, channels, height, width = latents.shape
313
+ latents = latents.view(batch_size, channels, height // patch_size, patch_size, width // patch_size, patch_size)
314
+ latents = latents.permute(0, 2, 4, 1, 3, 5)
315
+ return latents.reshape(
316
+ batch_size, (height // patch_size) * (width // patch_size), channels * patch_size * patch_size
317
+ )
318
+
319
+
320
+ def _reference_position_ids(index: int, grid_height: int, grid_width: int) -> torch.Tensor:
321
+ """`(grid_h * grid_w, 3)` rotary coordinates for reference `index`: frame axis `index + 1`, own y/x grid."""
322
+ ids = torch.zeros(grid_height, grid_width, 3)
323
+ ids[..., 0] = index + 1
324
+ ids[..., 1] = torch.arange(grid_height)[:, None]
325
+ ids[..., 2] = torch.arange(grid_width)[None, :]
326
+ return ids.reshape(-1, 3)
327
+
328
+
329
+ def _register_zero_time_reference_hooks(transformer, timestep, batch_size, split, total):
330
+ """Modulate the trailing `total - split` reference tokens at flow time `t=0` for the `append` mode.
331
+
332
+ A `forward_pre_hook` swaps each block's broadcast `(B, 1, 6 * hidden)` timestep embedding for a per-token
333
+ `(B, total, 6 * hidden)` one carrying the real timestep on the text and target rows and the `t=0` embedding
334
+ on the reference rows. The block's `temb.unflatten(-1, (6, -1)) + scale_shift_table` already broadcasts over
335
+ the sequence axis, so no source patching is needed.
336
+
337
+ Returns the hook handles; the caller must remove them once the forward pass is done.
338
+ """
339
+ dtype = transformer.dtype
340
+
341
+ def time_mod(t):
342
+ return transformer.time_mod_proj(F.gelu(transformer.time_embed(t, dtype=dtype), approximate="tanh"))
343
+
344
+ temb = time_mod(timestep)
345
+ temb_zero = time_mod(torch.zeros_like(timestep))
346
+ per_token = torch.cat([temb.expand(batch_size, split, -1), temb_zero.expand(batch_size, total - split, -1)], dim=1)
347
+
348
+ def pre_hook(module, args, kwargs):
349
+ if len(args) >= 2:
350
+ return (args[0], per_token, *args[2:]), kwargs
351
+ return args, {**kwargs, "temb": per_token}
352
+
353
+ return [block.register_forward_pre_hook(pre_hook, with_kwargs=True) for block in transformer.transformer_blocks]
354
+
355
+
356
+ def _concat_reference_latents(latents, reference_latents, mode, dtype):
357
+ """Splice the clean reference tokens into the noisy latents in the order the mode dictates.
358
+
359
+ Returns `(latents, reference_seq_len)`; `reference_seq_len` is 0 when the mode carries no latent channel.
360
+ """
361
+ if mode == REFERENCE_MODE_OFF or reference_latents is None:
362
+ return latents, 0
363
+ reference_latents = reference_latents.to(device=latents.device, dtype=dtype)
364
+ if reference_latents.shape[0] != latents.shape[0]:
365
+ reference_latents = reference_latents.expand(latents.shape[0], -1, -1)
366
+ if mode == REFERENCE_MODE_PREPEND:
367
+ return torch.cat([reference_latents, latents], dim=1), reference_latents.shape[1]
368
+ return torch.cat([latents, reference_latents], dim=1), reference_latents.shape[1]
369
+
370
+
371
+ def _slice_reference_rows(noise_pred, mode, reference_seq_len, target_seq_len):
372
+ """Drop the reference rows from a prediction, wherever the mode put them."""
373
+ if not reference_seq_len:
374
+ return noise_pred
375
+ if mode == REFERENCE_MODE_PREPEND:
376
+ return noise_pred[:, reference_seq_len:]
377
+ return noise_pred[:, :target_seq_len]
378
+
379
+
380
+ # auto_docstring
381
+ class Krea2ReferenceImagesStep(ModularPipelineBlocks):
382
+ """Base reference collector. Not used directly -- pick the subclass for the mode."""
383
+
384
+ model_name = "krea2"
385
+
386
+ uses_reference_latents = False
387
+
388
+ def _vision_view(self, image, block_state):
389
+ raise NotImplementedError
390
+
391
+ @property
392
+ def description(self) -> str:
393
+ return (
394
+ "Collect the reference images and size them for this mode's Qwen3-VL vision view, normalizing the "
395
+ "per-image style/subject strengths and masks."
396
+ )
397
+
398
+ @property
399
+ def inputs(self) -> list[InputParam]:
400
+ return [
401
+ InputParam(
402
+ name="reference_images",
403
+ type_hint=PIL.Image.Image | list[PIL.Image.Image],
404
+ description="Reference image(s) to condition on. A single image or a list.",
405
+ ),
406
+ InputParam(
407
+ name="reference_mode",
408
+ type_hint=str,
409
+ default=REFERENCE_MODE_OFF,
410
+ description=(
411
+ "How references condition the model: 'off' (Qwen3-VL vision path only, no LoRA), 'append' "
412
+ "(vision + clean VAE tokens after the target, Ostris edit LoRA) or 'prepend' (vision + "
413
+ "clean VAE source before the target, Identity-Edit LoRA)."
414
+ ),
415
+ ),
416
+ InputParam(
417
+ name="reference_style_strength",
418
+ type_hint=float | list[float],
419
+ default=1.0,
420
+ description=(
421
+ "Per-image scale on the deepstack vision features (low-level texture/style). A scalar "
422
+ "applies to every reference; 0 removes the channel."
423
+ ),
424
+ ),
425
+ InputParam(
426
+ name="reference_subject_strength",
427
+ type_hint=float | list[float],
428
+ default=1.0,
429
+ description=(
430
+ "Per-image scale on the in-sequence image tokens (content/subject). A scalar applies to "
431
+ "every reference; 0 removes the channel."
432
+ ),
433
+ ),
434
+ InputParam(
435
+ name="reference_masks",
436
+ type_hint=PIL.Image.Image | list[PIL.Image.Image],
437
+ description=(
438
+ "Optional per-image masks restricting each reference to part of the picture: painted is "
439
+ "the part to use. `reference_mask_mode` decides what happens to the rest. Index-aligned "
440
+ "to `reference_images`; use `None` for slots without a mask."
441
+ ),
442
+ ),
443
+ ]
444
+
445
+ @property
446
+ def intermediate_outputs(self) -> list[OutputParam]:
447
+ return [
448
+ OutputParam(name="reference_mode", type_hint=str, description="The normalized reference mode."),
449
+ OutputParam(
450
+ name="vision_reference_images",
451
+ type_hint=list,
452
+ description="References sized for the Qwen3-VL vision path (empty when there are none).",
453
+ ),
454
+ OutputParam(
455
+ name="vae_reference_images",
456
+ type_hint=list,
457
+ description="References for the VAE reference-latent channel (empty outside the latent modes).",
458
+ ),
459
+ OutputParam(
460
+ name="reference_style_strengths", type_hint=list, description="One style scale per reference."
461
+ ),
462
+ OutputParam(
463
+ name="reference_subject_strengths", type_hint=list, description="One subject scale per reference."
464
+ ),
465
+ OutputParam(name="reference_masks", type_hint=list, description="One mask (or None) per reference."),
466
+ ]
467
+
468
+ @torch.no_grad()
469
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
470
+ block_state = self.get_block_state(state)
471
+
472
+ images = [image for image in _as_list(block_state.reference_images) if image is not None]
473
+
474
+ block_state.reference_mode = self.reference_mode
475
+ block_state.reference_style_strengths = _broadcast_per_image(
476
+ block_state.reference_style_strength, len(images), 1.0
477
+ )
478
+ block_state.reference_subject_strengths = _broadcast_per_image(
479
+ block_state.reference_subject_strength, len(images), 1.0
480
+ )
481
+ masks = _as_list(block_state.reference_masks)
482
+ block_state.reference_masks = [masks[i] if i < len(masks) else None for i in range(len(images))]
483
+
484
+ block_state.vision_reference_images = [self._vision_view(image, block_state) for image in images]
485
+ block_state.vae_reference_images = [_to_rgb(image) for image in images] if self.uses_reference_latents else []
486
+
487
+ self.set_block_state(state, block_state)
488
+ return components, state
489
+
490
+
491
+ # auto_docstring
492
+ class Krea2VisionReferenceImagesStep(Krea2ReferenceImagesStep):
493
+ """Reference collector for the vision-only mode: a ~1 MP Qwen3-VL view, no VAE latent channel."""
494
+
495
+ reference_mode = REFERENCE_MODE_OFF
496
+ uses_reference_latents = False
497
+
498
+ @property
499
+ def description(self) -> str:
500
+ return "Collect the references and give each a ~1 MP Qwen3-VL view. Vision-only: no VAE latent channel."
501
+
502
+ def _vision_view(self, image, block_state):
503
+ return _cap_area(image, VISION_MAX_PIXELS)
504
+
505
+
506
+ # auto_docstring
507
+ class Krea2AppendReferenceImagesStep(Krea2ReferenceImagesStep):
508
+ """Reference collector for the Ostris `append` mode: a coarse ~384px Qwen3-VL view plus the VAE channel."""
509
+
510
+ reference_mode = REFERENCE_MODE_APPEND
511
+ uses_reference_latents = True
512
+
513
+ @property
514
+ def description(self) -> str:
515
+ return (
516
+ "Collect the references with a coarse ~384px Qwen3-VL view; the detail rides the VAE "
517
+ "reference-latent channel instead."
518
+ )
519
+
520
+ def _vision_view(self, image, block_state):
521
+ return _cap_area(image, VISION_EDIT_MAX_PIXELS)
522
+
523
+
524
+ # auto_docstring
525
+ class Krea2PrependReferenceImagesStep(Krea2ReferenceImagesStep):
526
+ """
527
+ Reference collector for the Identity-Edit `prepend` mode: a `grounding_px` longest-side Qwen3-VL view plus
528
+ the VAE channel.
529
+ """
530
+
531
+ reference_mode = REFERENCE_MODE_PREPEND
532
+ uses_reference_latents = True
533
+
534
+ @property
535
+ def description(self) -> str:
536
+ return (
537
+ "Collect the references with a `grounding_px` longest-side Qwen3-VL view, plus the images for the "
538
+ "VAE reference-latent channel."
539
+ )
540
+
541
+ @property
542
+ def inputs(self) -> list[InputParam]:
543
+ return super().inputs + [
544
+ InputParam(
545
+ name="grounding_px",
546
+ type_hint=int,
547
+ default=DEFAULT_GROUNDING_PX,
548
+ description=(
549
+ "Longest-side cap (px) on the Qwen3-VL view; the identity-vs-adherence dial. Lower is a "
550
+ "stronger edit, higher is stronger identity, 0 means native resolution."
551
+ ),
552
+ )
553
+ ]
554
+
555
+ def _vision_view(self, image, block_state):
556
+ return _cap_longest_side(image, int(block_state.grounding_px or 0))
557
+
558
+
559
+ # auto_docstring
560
+ class Krea2ReferenceTextEncoderStep(Krea2TextEncoderStep):
561
+ """
562
+ Text encoder step that feeds the reference images through Qwen3-VL's vision tower alongside the prompt,
563
+ keeping Krea 2's descriptor system template and 12-layer tap but inserting vision placeholders in the user
564
+ turn. Falls back to the stock text-only encode when there are no references.
565
+ """
566
+
567
+ model_name = "krea2"
568
+
569
+ # Identity-Edit trains its unconditional as the same source image with an empty instruction, so only that
570
+ # mode grounds the negative branch.
571
+ grounds_negative = False
572
+
573
+ def __init__(self):
574
+ super().__init__()
575
+ self._processor = None
576
+ self._processor_tokenizer = None
577
+
578
+ @property
579
+ def description(self) -> str:
580
+ return (
581
+ "Text encoder step that feeds the reference images through Qwen3-VL's vision tower alongside the "
582
+ "prompt, scaling each reference's vision features by its per-image style/subject strengths and "
583
+ "keeping only what a painted mask selects. Falls back to the stock text-only encode with no "
584
+ "references."
585
+ )
586
+
587
+ @property
588
+ def expected_components(self) -> list[ComponentSpec]:
589
+ return super().expected_components + [
590
+ ComponentSpec("processor", Qwen3VLProcessor, description="The Qwen3-VL vision processor.")
591
+ ]
592
+
593
+ @property
594
+ def inputs(self) -> list[InputParam]:
595
+ return super().inputs + [
596
+ InputParam(
597
+ name="reference_mode",
598
+ type_hint=str,
599
+ default=REFERENCE_MODE_OFF,
600
+ description="The normalized reference mode from the reference-images step.",
601
+ ),
602
+ InputParam(
603
+ name="vision_reference_images",
604
+ type_hint=list,
605
+ description="References sized for the Qwen3-VL vision path.",
606
+ ),
607
+ InputParam(name="reference_style_strengths", type_hint=list, description="One style scale per reference."),
608
+ InputParam(
609
+ name="reference_subject_strengths", type_hint=list, description="One subject scale per reference."
610
+ ),
611
+ InputParam(name="reference_masks", type_hint=list, description="One mask (or None) per reference."),
612
+ InputParam(
613
+ name="reference_mask_mode",
614
+ type_hint=str,
615
+ default=REFERENCE_MASK_MODE_EXCLUDE_BLANK,
616
+ description=(
617
+ "What a painted mask does to the vision path: 'exclude_blank' (default, mask the vision "
618
+ "attention and blank the masked-out tokens), 'exclude' (mask the attention only, leaving "
619
+ "the masked-out tokens in the sequence) or 'deemphasize' (blank the tokens only, which "
620
+ "attenuates the region rather than removing it)."
621
+ ),
622
+ ),
623
+ ]
624
+
625
+ def _get_processor(self, components):
626
+ """The declared `processor` component, or one derived from the text encoder when the repo has none."""
627
+ if components.processor is not None:
628
+ return components.processor
629
+ tokenizer = components.tokenizer
630
+ if self._processor is None or self._processor_tokenizer is not tokenizer:
631
+ self._processor = _build_reference_processor(components.text_encoder, tokenizer)
632
+ self._processor_tokenizer = tokenizer
633
+ return self._processor
634
+
635
+ def _image_prompt(self, images) -> str:
636
+ """The vision-token layout for this mode's training template."""
637
+ raise NotImplementedError
638
+
639
+ def _encode_prompt_with_vision(
640
+ self, components, prompts, images, style_strengths, subject_strengths, masks, device, mask_mode
641
+ ):
642
+ """Encode `prompts` with `images` in the user turn, returning `(hidden_states, attention_mask)`.
643
+
644
+ Unlike the text-only path this is variable length: the processor expands each `<|image_pad|>` to the
645
+ image's token count and right-pads the batch. The vision-token layout comes from `_image_prompt`.
646
+ """
647
+ processor = self._get_processor(components)
648
+ text_encoder = components.text_encoder
649
+ prefix_idx = _PROMPT_TEMPLATE_ENCODE_START_IDX
650
+
651
+ image_prompt = self._image_prompt(images)
652
+
653
+ # The stock prefix/suffix keep the leading block byte-identical to the text-only path, so the same
654
+ # `prefix_idx` drop still holds; the vision tokens sit after it and survive.
655
+ text = [
656
+ _PROMPT_TEMPLATE_ENCODE_PREFIX + image_prompt + (p or "") + _PROMPT_TEMPLATE_ENCODE_SUFFIX for p in prompts
657
+ ]
658
+ # Every prompt in the batch gets its own copy of the reference images, in placeholder order.
659
+ model_inputs = processor(text=text, images=list(images) * len(prompts), padding=True, return_tensors="pt")
660
+ model_inputs = model_inputs.to(device)
661
+
662
+ # Skip the `get_image_features` wrap entirely when every strength is 1.0 and nothing is masked, so the
663
+ # plain full-strength path stays byte-identical to an unwrapped encode.
664
+ repeats = len(prompts)
665
+ style = list(style_strengths) * repeats
666
+ subject = list(subject_strengths) * repeats
667
+ masks = list(masks) * repeats
668
+
669
+ has_pixels = model_inputs.get("pixel_values") is not None
670
+ needs_scaling = any(float(s) != 1.0 for s in style) or any(float(s) != 1.0 for s in subject)
671
+ has_masks = any(m is not None for m in masks)
672
+
673
+ # The mode selects which of the two independent layers a mask acts on.
674
+ mask_attention = has_masks and mask_mode != REFERENCE_MASK_MODE_DEEMPHASIZE
675
+ zero_masked_tokens = mask_mode != REFERENCE_MASK_MODE_EXCLUDE
676
+
677
+ keep_vectors = []
678
+ if has_pixels and zero_masked_tokens and (needs_scaling or has_masks):
679
+ keep_vectors = _build_keep_vectors(
680
+ masks, model_inputs.get("image_grid_thw"), processor.image_processor.merge_size
681
+ )
682
+ apply_scaling = has_pixels and (needs_scaling or any(k is not None for k in keep_vectors))
683
+
684
+ # Only the attention layer can keep unpainted content out of the reference; the output tokens the
685
+ # wrapper zeroes are whole-image summaries, so zeroing them attenuates but never excludes.
686
+ patch_keeps = []
687
+ if has_pixels and mask_attention:
688
+ patch_keeps = build_patch_keeps(
689
+ masks,
690
+ model_inputs.get("image_grid_thw"),
691
+ processor.image_processor.merge_size,
692
+ _mask_to_keep_grid,
693
+ )
694
+
695
+ original_get_image_features = None
696
+ if apply_scaling:
697
+ original_get_image_features = text_encoder.get_image_features
698
+ text_encoder.get_image_features = _scaled_get_image_features(
699
+ original_get_image_features, style, subject, processor.image_processor.merge_size, keep_vectors
700
+ )
701
+ try:
702
+ # No `position_ids`: with `pixel_values` present Qwen3-VL derives its mRoPE positions internally.
703
+ with vision_key_masks(patch_keeps):
704
+ outputs = text_encoder(
705
+ input_ids=model_inputs["input_ids"],
706
+ attention_mask=model_inputs["attention_mask"],
707
+ pixel_values=model_inputs.get("pixel_values"),
708
+ image_grid_thw=model_inputs.get("image_grid_thw"),
709
+ mm_token_type_ids=model_inputs.get("mm_token_type_ids"),
710
+ output_hidden_states=True,
711
+ )
712
+ finally:
713
+ if original_get_image_features is not None:
714
+ text_encoder.get_image_features = original_get_image_features
715
+
716
+ hidden_states = torch.stack([outputs.hidden_states[i] for i in KREA2_TEXT_ENCODER_SELECT_LAYERS], dim=2)
717
+ attention_mask = model_inputs["attention_mask"].bool()
718
+ return hidden_states[:, prefix_idx:], attention_mask[:, prefix_idx:]
719
+
720
+ @torch.no_grad()
721
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
722
+ block_state = self.get_block_state(state)
723
+
724
+ device = components._execution_device
725
+ prompts = [block_state.prompt] if isinstance(block_state.prompt, str) else list(block_state.prompt)
726
+ images = list(block_state.vision_reference_images or [])
727
+ mask_mode = _coerce_reference_mask_mode(block_state.reference_mask_mode)
728
+
729
+ if images:
730
+ block_state.prompt_embeds, block_state.prompt_embeds_mask = self._encode_prompt_with_vision(
731
+ components,
732
+ prompts,
733
+ images,
734
+ block_state.reference_style_strengths or [],
735
+ block_state.reference_subject_strengths or [],
736
+ block_state.reference_masks or [],
737
+ device,
738
+ mask_mode,
739
+ )
740
+ else:
741
+ block_state.prompt_embeds, block_state.prompt_embeds_mask = self._encode_prompt(
742
+ components, prompts, block_state.max_sequence_length, device
743
+ )
744
+
745
+ block_state.negative_prompt_embeds = None
746
+ block_state.negative_prompt_embeds_mask = None
747
+ if components.requires_unconditional_embeds:
748
+ negative_prompt = block_state.negative_prompt
749
+ if negative_prompt is None:
750
+ negative_prompt = ""
751
+ if isinstance(negative_prompt, str):
752
+ negative_prompt = [negative_prompt] * len(prompts)
753
+
754
+ if self.grounds_negative and images:
755
+ # The negative MUST get the same masks as the positive: an unmasked negative sees the whole
756
+ # reference, so (cond - uncond) pushes the unpainted region away instead of ignoring it.
757
+ # Strengths stay at full -- that is the training condition.
758
+ block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask = (
759
+ self._encode_prompt_with_vision(
760
+ components,
761
+ negative_prompt,
762
+ images,
763
+ [],
764
+ [],
765
+ block_state.reference_masks or [],
766
+ device,
767
+ mask_mode,
768
+ )
769
+ )
770
+ else:
771
+ block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask = self._encode_prompt(
772
+ components, negative_prompt, block_state.max_sequence_length, device
773
+ )
774
+
775
+ self.set_block_state(state, block_state)
776
+ return components, state
777
+
778
+
779
+ # auto_docstring
780
+ class Krea2VisionReferenceTextEncoderStep(Krea2ReferenceTextEncoderStep):
781
+ """
782
+ Text encoder for the vision-only mode. A single reference goes in as a bare vision block; several are
783
+ labelled "Picture N:". The negative branch stays text-only.
784
+ """
785
+
786
+ grounds_negative = False
787
+
788
+ @property
789
+ def description(self) -> str:
790
+ return "Text encoder for the vision-only mode: references enter the prompt through Qwen3-VL's vision tower."
791
+
792
+ def _image_prompt(self, images) -> str:
793
+ if len(images) > 1:
794
+ return "".join(f"Picture {i + 1}: <|vision_start|><|image_pad|><|vision_end|>" for i in range(len(images)))
795
+ return "<|vision_start|><|image_pad|><|vision_end|>"
796
+
797
+
798
+ # auto_docstring
799
+ class Krea2AppendReferenceTextEncoderStep(Krea2ReferenceTextEncoderStep):
800
+ """
801
+ Text encoder for the Ostris `append` mode, which labels every reference "Picture N:" even when there is only
802
+ one. The negative branch stays text-only.
803
+ """
804
+
805
+ grounds_negative = False
806
+
807
+ @property
808
+ def description(self) -> str:
809
+ return 'Text encoder for the Ostris append mode: every reference is labelled "Picture N:".'
810
+
811
+ def _image_prompt(self, images) -> str:
812
+ return "".join(f"Picture {i + 1}: <|vision_start|><|image_pad|><|vision_end|>" for i in range(len(images)))
813
+
814
+
815
+ # auto_docstring
816
+ class Krea2PrependReferenceTextEncoderStep(Krea2ReferenceTextEncoderStep):
817
+ """Text encoder for the Identity-Edit `prepend` mode: unlabelled vision blocks, grounded negative branch."""
818
+
819
+ grounds_negative = True
820
+
821
+ @property
822
+ def description(self) -> str:
823
+ return (
824
+ "Text encoder for the Identity-Edit prepend mode: bare vision blocks and a negative branch grounded "
825
+ "on the same source, as the LoRA was trained."
826
+ )
827
+
828
+ def _image_prompt(self, images) -> str:
829
+ return "".join("<|vision_start|><|image_pad|><|vision_end|>" for _ in images)
830
+
831
+
832
+ # auto_docstring
833
+ class Krea2ReferenceTextInputsStep(ModularPipelineBlocks):
834
+ """
835
+ Input step that determines `batch_size`/`dtype` from the per-prompt `prompt_embeds` and replicates the text
836
+ conditioning (and the negative branch) to `batch_size * num_images_per_prompt`. Unlike the stock step it
837
+ lets the two branches carry different sequence lengths.
838
+ """
839
+
840
+ model_name = "krea2"
841
+
842
+ @property
843
+ def description(self) -> str:
844
+ return (
845
+ "Input step that determines `batch_size`/`dtype` and batch-expands the text conditioning, allowing "
846
+ "the positive and negative branches to carry different sequence lengths (the positive gains vision "
847
+ "tokens the text-only negative does not have)."
848
+ )
849
+
850
+ @property
851
+ def inputs(self) -> list[InputParam]:
852
+ return [
853
+ InputParam.template("num_images_per_prompt", default=1),
854
+ InputParam.template("prompt_embeds"),
855
+ InputParam.template("prompt_embeds_mask"),
856
+ InputParam.template("negative_prompt_embeds"),
857
+ InputParam.template("negative_prompt_embeds_mask"),
858
+ ]
859
+
860
+ @property
861
+ def intermediate_outputs(self) -> list[OutputParam]:
862
+ return [
863
+ OutputParam(
864
+ name="batch_size",
865
+ type_hint=int,
866
+ description="Effective batch size (num prompts * num_images_per_prompt).",
867
+ ),
868
+ OutputParam(name="dtype", type_hint=torch.dtype, description="The dtype of the text features."),
869
+ OutputParam(name="prompt_embeds", type_hint=torch.Tensor, description="Text features, batch-expanded."),
870
+ OutputParam(name="prompt_embeds_mask", type_hint=torch.Tensor, description="Text mask, batch-expanded."),
871
+ OutputParam(
872
+ name="negative_prompt_embeds",
873
+ type_hint=torch.Tensor,
874
+ description="Negative text features, batch-expanded.",
875
+ ),
876
+ OutputParam(
877
+ name="negative_prompt_embeds_mask",
878
+ type_hint=torch.Tensor,
879
+ description="Negative text mask, batch-expanded.",
880
+ ),
881
+ ]
882
+
883
+ @staticmethod
884
+ def _expand(embeds, mask, num_images_per_prompt):
885
+ prompt_batch, seq_len, num_layers, dim = embeds.shape
886
+ n = num_images_per_prompt
887
+ embeds = embeds.repeat(1, n, 1, 1).view(prompt_batch * n, seq_len, num_layers, dim)
888
+ mask = mask.repeat(1, n).view(prompt_batch * n, seq_len)
889
+ return embeds, mask
890
+
891
+ @torch.no_grad()
892
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
893
+ block_state = self.get_block_state(state)
894
+
895
+ n = block_state.num_images_per_prompt
896
+ block_state.dtype = block_state.prompt_embeds.dtype
897
+ block_state.batch_size = block_state.prompt_embeds.shape[0] * n
898
+
899
+ block_state.prompt_embeds, block_state.prompt_embeds_mask = self._expand(
900
+ block_state.prompt_embeds, block_state.prompt_embeds_mask, n
901
+ )
902
+ if block_state.negative_prompt_embeds is not None:
903
+ block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask = self._expand(
904
+ block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask, n
905
+ )
906
+
907
+ self.set_block_state(state, block_state)
908
+ return components, state
909
+
910
+
911
+ # auto_docstring
912
+ class Krea2ReferenceLatentsStep(ModularPipelineBlocks):
913
+ """
914
+ VAE-encode the reference images into clean latent tokens for the `append`/`prepend` conditioning paths.
915
+ Each reference is encoded at up to ~1 MP with its own aspect ratio, normalized with the VAE's per-channel
916
+ statistics, patch-packed exactly like the noise latents and given Kontext-style rotary coordinates: the
917
+ i-th reference sits on rotary frame axis `i + 1` with its own y/x grid. Reference masks reach this channel
918
+ only when `mask_reference_latents` is set. A no-op in `off` mode or with no references.
919
+ """
920
+
921
+ model_name = "krea2"
922
+
923
+ @property
924
+ def description(self) -> str:
925
+ return (
926
+ "VAE-encode the reference images into clean, patch-packed latent tokens with frame-axis rotary "
927
+ "coordinates for the `append`/`prepend` conditioning paths. A no-op in `off` mode."
928
+ )
929
+
930
+ @property
931
+ def expected_components(self) -> list[ComponentSpec]:
932
+ return [ComponentSpec("vae", AutoencoderKLQwenImage)]
933
+
934
+ @property
935
+ def inputs(self) -> list[InputParam]:
936
+ return [
937
+ InputParam(
938
+ name="reference_mode",
939
+ type_hint=str,
940
+ default=REFERENCE_MODE_OFF,
941
+ description="The normalized reference mode from the reference-images step.",
942
+ ),
943
+ InputParam(
944
+ name="vae_reference_images",
945
+ type_hint=list,
946
+ description="References for the VAE reference-latent channel.",
947
+ ),
948
+ InputParam(name="reference_masks", type_hint=list, description="One mask (or None) per reference."),
949
+ InputParam(
950
+ name="mask_reference_latents",
951
+ type_hint=bool,
952
+ default=False,
953
+ description=(
954
+ "Apply the reference masks to this channel too, on top of the vision tokens they always "
955
+ "mask. Off (default) is vision-only masking, so the edit LoRA still sees the whole "
956
+ "reference at full detail -- which is why a masked edit can show hints of the unpainted "
957
+ "parts. On drops the unpainted latent patch tokens from the sequence entirely."
958
+ ),
959
+ ),
960
+ InputParam.template("dtype"),
961
+ ]
962
+
963
+ @property
964
+ def intermediate_outputs(self) -> list[OutputParam]:
965
+ return [
966
+ OutputParam(
967
+ name="reference_latents",
968
+ type_hint=torch.Tensor,
969
+ description="Clean packed reference tokens (1, reference_seq_len, in_channels), or None.",
970
+ ),
971
+ OutputParam(
972
+ name="reference_position_ids",
973
+ type_hint=torch.Tensor,
974
+ description="Rotary coordinates for the reference tokens (reference_seq_len, 3), or None.",
975
+ ),
976
+ OutputParam(
977
+ name="reference_seq_len", type_hint=int, description="Number of reference tokens (0 when unused)."
978
+ ),
979
+ ]
980
+
981
+ @staticmethod
982
+ def _to_snapped_tensor(image, snap: int) -> torch.Tensor:
983
+ """Reference image -> normalized `(1, 3, 1, H, W)` tensor for the temporal VAE.
984
+
985
+ Aspect-preserving downscale to fit `REFERENCE_LATENTS_MAX_PIXELS` (never upscaled), then snap each side
986
+ to a multiple of `snap` so the latent grid is patchifiable.
987
+ """
988
+ image = _to_rgb(image)
989
+ width, height = image.width, image.height
990
+ if height * width > REFERENCE_LATENTS_MAX_PIXELS:
991
+ ratio = height / width
992
+ new_height = math.sqrt(REFERENCE_LATENTS_MAX_PIXELS * ratio)
993
+ new_width = math.sqrt(REFERENCE_LATENTS_MAX_PIXELS / ratio)
994
+ else:
995
+ new_height, new_width = float(height), float(width)
996
+ new_height = max(snap, int(round(new_height / snap)) * snap)
997
+ new_width = max(snap, int(round(new_width / snap)) * snap)
998
+
999
+ array = np.asarray(image, dtype=np.float32) / 255.0
1000
+ tensor = torch.from_numpy(array).permute(2, 0, 1).unsqueeze(0) * 2.0 - 1.0
1001
+ if (new_height, new_width) != (height, width):
1002
+ tensor = F.interpolate(tensor, size=(new_height, new_width), mode="bilinear", align_corners=False)
1003
+ return tensor.unsqueeze(2)
1004
+
1005
+ @torch.no_grad()
1006
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
1007
+ block_state = self.get_block_state(state)
1008
+
1009
+ images = list(block_state.vae_reference_images or [])
1010
+ if not images:
1011
+ raise ValueError(
1012
+ "The append and prepend modes need at least one reference image; pass `reference_images` or "
1013
+ "switch to the vision-only mode."
1014
+ )
1015
+
1016
+ block_state.reference_latents = None
1017
+ block_state.reference_position_ids = None
1018
+ block_state.reference_seq_len = 0
1019
+
1020
+ vae = components.vae
1021
+ patch_size = components.patch_size
1022
+ snap = components.vae_scale_factor * patch_size
1023
+
1024
+ z_dim = vae.config.z_dim
1025
+ latents_mean = torch.tensor(vae.config.latents_mean).view(1, z_dim, 1, 1, 1)
1026
+ latents_std = torch.tensor(vae.config.latents_std).view(1, z_dim, 1, 1, 1)
1027
+
1028
+ masks = list(block_state.reference_masks or [])
1029
+ mask_latents = bool(block_state.mask_reference_latents)
1030
+
1031
+ tokens = []
1032
+ position_ids = []
1033
+ for i, image in enumerate(images):
1034
+ pixels = self._to_snapped_tensor(image, snap).to(vae.device, vae.dtype)
1035
+ raw = vae.encode(pixels).latent_dist.mode()
1036
+ mean = latents_mean.to(raw.device, raw.dtype)
1037
+ std = latents_std.to(raw.device, raw.dtype)
1038
+ latents = ((raw - mean) / std)[:, :, 0] # drop the temporal axis -> (1, C, lat_h, lat_w)
1039
+
1040
+ image_tokens = _pack_latents(latents, patch_size).to(block_state.dtype)
1041
+ _, _, latent_height, latent_width = latents.shape
1042
+ grid_height, grid_width = latent_height // patch_size, latent_width // patch_size
1043
+ image_position_ids = _reference_position_ids(i, grid_height, grid_width)
1044
+
1045
+ if mask_latents:
1046
+ keep = _mask_to_keep_vector(masks[i] if i < len(masks) else None, grid_height, grid_width)
1047
+ if keep is not None:
1048
+ # Dropped rather than zeroed: a zero token is the mean latent (flat mid-grey), which the
1049
+ # model can still attend to and reproduce. Position ids go with them, so the kept tokens
1050
+ # keep their true coordinates.
1051
+ index = torch.nonzero(keep > 0.5, as_tuple=False).squeeze(-1)
1052
+ if index.numel() == 0:
1053
+ continue
1054
+ image_tokens = image_tokens[:, index]
1055
+ image_position_ids = image_position_ids[index]
1056
+
1057
+ tokens.append(image_tokens)
1058
+ position_ids.append(image_position_ids)
1059
+
1060
+ block_state.reference_latents = torch.cat(tokens, dim=1)
1061
+ block_state.reference_position_ids = torch.cat(position_ids, dim=0)
1062
+ block_state.reference_seq_len = int(block_state.reference_latents.shape[1])
1063
+
1064
+ self.set_block_state(state, block_state)
1065
+ return components, state
1066
+
1067
+
1068
+ # auto_docstring
1069
+ class Krea2ReferencePreparePositionIdsStep(ModularPipelineBlocks):
1070
+ """
1071
+ Build the rotary position ids for both guidance branches, which carry different text lengths once the
1072
+ positive gains vision tokens. Reference-latent coordinates are spliced in before the target for `prepend`
1073
+ and after it for `append`.
1074
+ """
1075
+
1076
+ model_name = "krea2"
1077
+
1078
+ @property
1079
+ def description(self) -> str:
1080
+ return (
1081
+ "Build the rotary position ids for both guidance branches (the branches carry different text "
1082
+ "lengths once the positive gains vision tokens), splicing in the reference-latent coordinates "
1083
+ "before the target for `prepend` and after it for `append`."
1084
+ )
1085
+
1086
+ @property
1087
+ def inputs(self) -> list[InputParam]:
1088
+ return [
1089
+ InputParam.template("height", default=1024),
1090
+ InputParam.template("width", default=1024),
1091
+ InputParam.template("prompt_embeds"),
1092
+ InputParam.template("negative_prompt_embeds"),
1093
+ InputParam(
1094
+ name="reference_mode",
1095
+ type_hint=str,
1096
+ default=REFERENCE_MODE_OFF,
1097
+ description="The normalized reference mode from the reference-images step.",
1098
+ ),
1099
+ InputParam(
1100
+ name="reference_position_ids",
1101
+ type_hint=torch.Tensor,
1102
+ description="Rotary coordinates for the reference tokens, when the latent modes are active.",
1103
+ ),
1104
+ ]
1105
+
1106
+ @property
1107
+ def intermediate_outputs(self) -> list[OutputParam]:
1108
+ return [
1109
+ OutputParam(
1110
+ name="position_ids",
1111
+ type_hint=torch.Tensor,
1112
+ description="Rotary coordinates for the conditional branch.",
1113
+ ),
1114
+ OutputParam(
1115
+ name="negative_position_ids",
1116
+ type_hint=torch.Tensor,
1117
+ description="Rotary coordinates for the unconditional branch.",
1118
+ ),
1119
+ ]
1120
+
1121
+ @torch.no_grad()
1122
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
1123
+ block_state = self.get_block_state(state)
1124
+
1125
+ device = components._execution_device
1126
+ patch_size = components.patch_size
1127
+ grid_h = block_state.height // (components.vae_scale_factor * patch_size)
1128
+ grid_w = block_state.width // (components.vae_scale_factor * patch_size)
1129
+ reference_ids = block_state.reference_position_ids
1130
+ if reference_ids is not None:
1131
+ reference_ids = reference_ids.to(device)
1132
+
1133
+ def build(text_seq_len: int) -> torch.Tensor:
1134
+ ids = Krea2PreparePositionIdsStep.prepare_position_ids(text_seq_len, grid_h, grid_w, device)
1135
+ if reference_ids is None or self.reference_mode == REFERENCE_MODE_OFF:
1136
+ return ids
1137
+ if self.reference_mode == REFERENCE_MODE_PREPEND:
1138
+ # [text | reference | target]
1139
+ return torch.cat([ids[:text_seq_len], reference_ids, ids[text_seq_len:]], dim=0)
1140
+ # [text | target | reference]
1141
+ return torch.cat([ids, reference_ids], dim=0)
1142
+
1143
+ text_seq_len = block_state.prompt_embeds.shape[1]
1144
+ block_state.position_ids = build(text_seq_len)
1145
+
1146
+ negative_prompt_embeds = block_state.negative_prompt_embeds
1147
+ if negative_prompt_embeds is None or negative_prompt_embeds.shape[1] == text_seq_len:
1148
+ block_state.negative_position_ids = block_state.position_ids
1149
+ else:
1150
+ block_state.negative_position_ids = build(negative_prompt_embeds.shape[1])
1151
+
1152
+ self.set_block_state(state, block_state)
1153
+ return components, state
1154
+
1155
+
1156
+ class Krea2ReferenceLoopDenoiser(ModularPipelineBlocks):
1157
+ model_name = "krea2"
1158
+
1159
+ @property
1160
+ def description(self) -> str:
1161
+ return (
1162
+ "Within the denoising loop: concatenate the clean reference tokens onto the noisy latents in the "
1163
+ "order the mode dictates, run the `transformer` per guidance branch with that branch's own "
1164
+ "position ids, and slice the reference rows back off the prediction. Compose into "
1165
+ "`Krea2ReferenceDenoiseStep`."
1166
+ )
1167
+
1168
+ @property
1169
+ def expected_components(self) -> list[ComponentSpec]:
1170
+ return [
1171
+ ComponentSpec(
1172
+ "guider",
1173
+ ClassifierFreeGuidance,
1174
+ # Krea 2 uses cond-anchored CFG (`cond + scale * (cond - uncond)`), which is the
1175
+ # `use_original_formulation` branch.
1176
+ config=FrozenDict({"guidance_scale": 4.5, "use_original_formulation": True}),
1177
+ default_creation_method="from_config",
1178
+ ),
1179
+ ComponentSpec("transformer", Krea2Transformer2DModel),
1180
+ ]
1181
+
1182
+ @property
1183
+ def inputs(self) -> list[InputParam]:
1184
+ return [
1185
+ InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
1186
+ InputParam.template("num_inference_steps", required=True),
1187
+ InputParam.template("prompt_embeds"),
1188
+ InputParam.template("prompt_embeds_mask"),
1189
+ InputParam.template("negative_prompt_embeds"),
1190
+ InputParam.template("negative_prompt_embeds_mask"),
1191
+ InputParam(
1192
+ name="position_ids",
1193
+ required=True,
1194
+ type_hint=torch.Tensor,
1195
+ description="Rotary coordinates for the conditional branch.",
1196
+ ),
1197
+ InputParam(
1198
+ name="negative_position_ids",
1199
+ type_hint=torch.Tensor,
1200
+ description="Rotary coordinates for the unconditional branch.",
1201
+ ),
1202
+ InputParam(
1203
+ name="reference_mode",
1204
+ type_hint=str,
1205
+ default=REFERENCE_MODE_OFF,
1206
+ description="The normalized reference mode from the reference-images step.",
1207
+ ),
1208
+ InputParam(
1209
+ name="reference_latents",
1210
+ type_hint=torch.Tensor,
1211
+ description="Clean packed reference tokens, when the latent modes are active.",
1212
+ ),
1213
+ InputParam.template("attention_kwargs"),
1214
+ ]
1215
+
1216
+ @torch.no_grad()
1217
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
1218
+ transformer = components.transformer
1219
+
1220
+ latents = block_state.latents.to(transformer.dtype)
1221
+ timestep = block_state.timestep.to(transformer.dtype)
1222
+ target_seq_len = latents.shape[1]
1223
+
1224
+ latents, reference_seq_len = _concat_reference_latents(
1225
+ latents, block_state.reference_latents, self.reference_mode, transformer.dtype
1226
+ )
1227
+
1228
+ negative_position_ids = block_state.negative_position_ids
1229
+ if negative_position_ids is None:
1230
+ negative_position_ids = block_state.position_ids
1231
+
1232
+ guider_inputs = {
1233
+ "encoder_hidden_states": (
1234
+ block_state.prompt_embeds.to(transformer.dtype),
1235
+ block_state.negative_prompt_embeds.to(transformer.dtype)
1236
+ if block_state.negative_prompt_embeds is not None
1237
+ else None,
1238
+ ),
1239
+ "encoder_attention_mask": (
1240
+ block_state.prompt_embeds_mask,
1241
+ block_state.negative_prompt_embeds_mask,
1242
+ ),
1243
+ # A tuple is indexed per guidance branch, so each pass gets coordinates for its own text length.
1244
+ "position_ids": (block_state.position_ids, negative_position_ids),
1245
+ }
1246
+
1247
+ components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
1248
+ guider_state = components.guider.prepare_inputs(guider_inputs)
1249
+
1250
+ for guider_state_batch in guider_state:
1251
+ components.guider.prepare_models(transformer)
1252
+ cond_kwargs = {name: getattr(guider_state_batch, name) for name in guider_inputs}
1253
+
1254
+ handles = []
1255
+ if reference_seq_len and self.reference_mode == REFERENCE_MODE_APPEND:
1256
+ # The split index is branch-dependent: the conditional text block carries the vision tokens.
1257
+ text_seq_len = cond_kwargs["encoder_hidden_states"].shape[1]
1258
+ handles = _register_zero_time_reference_hooks(
1259
+ transformer,
1260
+ timestep,
1261
+ batch_size=latents.shape[0],
1262
+ split=text_seq_len + target_seq_len,
1263
+ total=text_seq_len + target_seq_len + reference_seq_len,
1264
+ )
1265
+ try:
1266
+ noise_pred = transformer(
1267
+ hidden_states=latents,
1268
+ timestep=timestep,
1269
+ attention_kwargs=block_state.attention_kwargs,
1270
+ return_dict=False,
1271
+ **cond_kwargs,
1272
+ )[0]
1273
+ finally:
1274
+ for handle in handles:
1275
+ handle.remove()
1276
+
1277
+ guider_state_batch.noise_pred = _slice_reference_rows(
1278
+ noise_pred, self.reference_mode, reference_seq_len, target_seq_len
1279
+ )
1280
+ components.guider.cleanup_models(transformer)
1281
+
1282
+ block_state.noise_pred = components.guider(guider_state).pred
1283
+ return components, block_state
1284
+
1285
+
1286
+ # auto_docstring
1287
+ class Krea2ReferenceDenoiseStep(Krea2DenoiseLoopWrapper):
1288
+ """
1289
+ Denoising loop that iteratively denoises the packed image latents over `timesteps` with reference
1290
+ conditioning: the clean reference tokens are concatenated in the order the mode dictates, each guidance
1291
+ branch runs with its own position ids, and the reference rows are sliced back off the prediction before the
1292
+ scheduler step.
1293
+ """
1294
+
1295
+ model_name = "krea2"
1296
+ block_classes = [Krea2LoopBeforeDenoiser, Krea2ReferenceLoopDenoiser, Krea2LoopAfterDenoiser]
1297
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
1298
+
1299
+ @property
1300
+ def description(self) -> str:
1301
+ return (
1302
+ "Denoising loop with reference conditioning: concatenates the clean reference tokens, runs each "
1303
+ "guidance branch with its own position ids, and slices the reference rows back off the prediction. "
1304
+ "Identical to the stock loop when no reference latents are active."
1305
+ )
1306
+
1307
+
1308
+ # =====================================================================================================
1309
+ # Per-mode leaf blocks
1310
+ # =====================================================================================================
1311
+
1312
+
1313
+ # auto_docstring
1314
+ class Krea2VisionReferencePositionIdsStep(Krea2ReferencePreparePositionIdsStep):
1315
+ """Rotary coordinates for the vision-only mode: per-branch, with no reference rows to splice in."""
1316
+
1317
+ reference_mode = REFERENCE_MODE_OFF
1318
+
1319
+ @property
1320
+ def description(self) -> str:
1321
+ return "Per-branch rotary coordinates for the [text | image] sequence (vision-only: no reference rows)."
1322
+
1323
+
1324
+ # auto_docstring
1325
+ class Krea2AppendReferencePositionIdsStep(Krea2ReferencePreparePositionIdsStep):
1326
+ """Rotary coordinates for the Ostris `append` mode: the reference rows go after the target."""
1327
+
1328
+ reference_mode = REFERENCE_MODE_APPEND
1329
+
1330
+ @property
1331
+ def description(self) -> str:
1332
+ return "Per-branch rotary coordinates with the reference rows appended after the target."
1333
+
1334
+
1335
+ # auto_docstring
1336
+ class Krea2PrependReferencePositionIdsStep(Krea2ReferencePreparePositionIdsStep):
1337
+ """Rotary coordinates for the Identity-Edit `prepend` mode: the reference rows go before the target."""
1338
+
1339
+ reference_mode = REFERENCE_MODE_PREPEND
1340
+
1341
+ @property
1342
+ def description(self) -> str:
1343
+ return "Per-branch rotary coordinates with the reference rows spliced in before the target."
1344
+
1345
+
1346
+ class Krea2VisionReferenceLoopDenoiser(Krea2ReferenceLoopDenoiser):
1347
+ reference_mode = REFERENCE_MODE_OFF
1348
+
1349
+ @property
1350
+ def description(self) -> str:
1351
+ return (
1352
+ "Within the denoising loop: run the `transformer` per guidance branch with that branch's own "
1353
+ "position ids. Vision-only, so there are no reference tokens in the sequence."
1354
+ )
1355
+
1356
+
1357
+ class Krea2AppendReferenceLoopDenoiser(Krea2ReferenceLoopDenoiser):
1358
+ reference_mode = REFERENCE_MODE_APPEND
1359
+
1360
+ @property
1361
+ def description(self) -> str:
1362
+ return (
1363
+ "Within the denoising loop: append the clean reference tokens after the noisy target, modulate them "
1364
+ "at flow time t=0, and slice them back off the prediction."
1365
+ )
1366
+
1367
+
1368
+ class Krea2PrependReferenceLoopDenoiser(Krea2ReferenceLoopDenoiser):
1369
+ reference_mode = REFERENCE_MODE_PREPEND
1370
+
1371
+ @property
1372
+ def description(self) -> str:
1373
+ return (
1374
+ "Within the denoising loop: prepend the clean reference tokens before the noisy target under the "
1375
+ "plain uniform modulation, and slice them back off the prediction."
1376
+ )
1377
+
1378
+
1379
+ # auto_docstring
1380
+ class Krea2VisionReferenceDenoiseStep(Krea2DenoiseLoopWrapper):
1381
+ """Denoising loop for the vision-only mode, with symmetric CFG."""
1382
+
1383
+ model_name = "krea2"
1384
+ block_classes = [Krea2LoopBeforeDenoiser, Krea2VisionReferenceLoopDenoiser, Krea2LoopAfterDenoiser]
1385
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
1386
+
1387
+ @property
1388
+ def description(self) -> str:
1389
+ return "Denoising loop for the vision-only mode, with per-branch position ids."
1390
+
1391
+
1392
+ # auto_docstring
1393
+ class Krea2AppendReferenceDenoiseStep(Krea2DenoiseLoopWrapper):
1394
+ """Denoising loop for the Ostris `append` mode: reference tokens at the tail, modulated at t=0."""
1395
+
1396
+ model_name = "krea2"
1397
+ block_classes = [Krea2LoopBeforeDenoiser, Krea2AppendReferenceLoopDenoiser, Krea2LoopAfterDenoiser]
1398
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
1399
+
1400
+ @property
1401
+ def description(self) -> str:
1402
+ return "Denoising loop for the Ostris append mode: clean reference tokens at the tail, modulated at t=0."
1403
+
1404
+
1405
+ # auto_docstring
1406
+ class Krea2PrependReferenceDenoiseStep(Krea2DenoiseLoopWrapper):
1407
+ """Denoising loop for the Identity-Edit `prepend` mode: the clean source sits before the target."""
1408
+
1409
+ model_name = "krea2"
1410
+ block_classes = [Krea2LoopBeforeDenoiser, Krea2PrependReferenceLoopDenoiser, Krea2LoopAfterDenoiser]
1411
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
1412
+
1413
+ @property
1414
+ def description(self) -> str:
1415
+ return "Denoising loop for the Identity-Edit prepend mode: the clean source before the noisy target."
1416
+
1417
+
1418
+ class _Krea2TurboTextEncoderMixin:
1419
+ """Drops the negative branch from a reference text encoder for the distilled checkpoint."""
1420
+
1421
+ @property
1422
+ def expected_components(self) -> list[ComponentSpec]:
1423
+ return [spec for spec in super().expected_components if spec.name != "guider"]
1424
+
1425
+ @property
1426
+ def inputs(self) -> list[InputParam]:
1427
+ return [param for param in super().inputs if param.name != "negative_prompt"]
1428
+
1429
+ @property
1430
+ def intermediate_outputs(self) -> list[OutputParam]:
1431
+ return [
1432
+ param
1433
+ for param in super().intermediate_outputs
1434
+ if param.name not in ("negative_prompt_embeds", "negative_prompt_embeds_mask")
1435
+ ]
1436
+
1437
+ @torch.no_grad()
1438
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
1439
+ block_state = self.get_block_state(state)
1440
+
1441
+ device = components._execution_device
1442
+ prompts = [block_state.prompt] if isinstance(block_state.prompt, str) else list(block_state.prompt)
1443
+ images = list(block_state.vision_reference_images or [])
1444
+ mask_mode = _coerce_reference_mask_mode(block_state.reference_mask_mode)
1445
+
1446
+ if images:
1447
+ block_state.prompt_embeds, block_state.prompt_embeds_mask = self._encode_prompt_with_vision(
1448
+ components,
1449
+ prompts,
1450
+ images,
1451
+ block_state.reference_style_strengths or [],
1452
+ block_state.reference_subject_strengths or [],
1453
+ block_state.reference_masks or [],
1454
+ device,
1455
+ mask_mode,
1456
+ )
1457
+ else:
1458
+ block_state.prompt_embeds, block_state.prompt_embeds_mask = self._encode_prompt(
1459
+ components, prompts, block_state.max_sequence_length, device
1460
+ )
1461
+
1462
+ self.set_block_state(state, block_state)
1463
+ return components, state
1464
+
1465
+
1466
+ # auto_docstring
1467
+ class Krea2TurboVisionReferenceTextEncoderStep(_Krea2TurboTextEncoderMixin, Krea2VisionReferenceTextEncoderStep):
1468
+ """Vision-only text encoder for the distilled checkpoint: no negative branch, no guider."""
1469
+
1470
+
1471
+ # auto_docstring
1472
+ class Krea2TurboAppendReferenceTextEncoderStep(_Krea2TurboTextEncoderMixin, Krea2AppendReferenceTextEncoderStep):
1473
+ """Ostris append text encoder for the distilled checkpoint: no negative branch, no guider."""
1474
+
1475
+
1476
+ # auto_docstring
1477
+ class Krea2TurboPrependReferenceTextEncoderStep(_Krea2TurboTextEncoderMixin, Krea2PrependReferenceTextEncoderStep):
1478
+ """Identity-Edit prepend text encoder for the distilled checkpoint: no negative branch, no guider."""
1479
+
1480
+
1481
+ class Krea2TurboReferenceLoopDenoiser(ModularPipelineBlocks):
1482
+ model_name = "krea2"
1483
+
1484
+ @property
1485
+ def description(self) -> str:
1486
+ return (
1487
+ "Within the denoising loop: run the `transformer` on the conditional text features with the clean "
1488
+ "reference tokens spliced in, then slice them back off. The distilled checkpoint runs without "
1489
+ "classifier-free guidance, so there is no negative branch or guider."
1490
+ )
1491
+
1492
+ @property
1493
+ def expected_components(self) -> list[ComponentSpec]:
1494
+ return [ComponentSpec("transformer", Krea2Transformer2DModel)]
1495
+
1496
+ @property
1497
+ def inputs(self) -> list[InputParam]:
1498
+ return [
1499
+ InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
1500
+ InputParam.template("prompt_embeds"),
1501
+ InputParam.template("prompt_embeds_mask"),
1502
+ InputParam(
1503
+ name="position_ids",
1504
+ required=True,
1505
+ type_hint=torch.Tensor,
1506
+ description="Rotary coordinates for the [text | image] sequence.",
1507
+ ),
1508
+ InputParam(
1509
+ name="reference_mode",
1510
+ type_hint=str,
1511
+ default=REFERENCE_MODE_OFF,
1512
+ description="The normalized reference mode from the reference-images step.",
1513
+ ),
1514
+ InputParam(
1515
+ name="reference_latents",
1516
+ type_hint=torch.Tensor,
1517
+ description="Clean packed reference tokens, when the latent modes are active.",
1518
+ ),
1519
+ InputParam.template("attention_kwargs"),
1520
+ ]
1521
+
1522
+ @torch.no_grad()
1523
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
1524
+ transformer = components.transformer
1525
+
1526
+ latents = block_state.latents.to(transformer.dtype)
1527
+ timestep = block_state.timestep.to(transformer.dtype)
1528
+ target_seq_len = latents.shape[1]
1529
+
1530
+ latents, reference_seq_len = _concat_reference_latents(
1531
+ latents, block_state.reference_latents, self.reference_mode, transformer.dtype
1532
+ )
1533
+
1534
+ handles = []
1535
+ if reference_seq_len and self.reference_mode == REFERENCE_MODE_APPEND:
1536
+ text_seq_len = block_state.prompt_embeds.shape[1]
1537
+ handles = _register_zero_time_reference_hooks(
1538
+ transformer,
1539
+ timestep,
1540
+ batch_size=latents.shape[0],
1541
+ split=text_seq_len + target_seq_len,
1542
+ total=text_seq_len + target_seq_len + reference_seq_len,
1543
+ )
1544
+ try:
1545
+ noise_pred = transformer(
1546
+ hidden_states=latents,
1547
+ timestep=timestep,
1548
+ position_ids=block_state.position_ids,
1549
+ attention_kwargs=block_state.attention_kwargs,
1550
+ encoder_hidden_states=block_state.prompt_embeds.to(transformer.dtype),
1551
+ encoder_attention_mask=block_state.prompt_embeds_mask,
1552
+ return_dict=False,
1553
+ )[0]
1554
+ finally:
1555
+ for handle in handles:
1556
+ handle.remove()
1557
+
1558
+ block_state.noise_pred = _slice_reference_rows(
1559
+ noise_pred, self.reference_mode, reference_seq_len, target_seq_len
1560
+ )
1561
+ return components, block_state
1562
+
1563
+
1564
+ # auto_docstring
1565
+ class Krea2TurboReferenceDenoiseStep(Krea2DenoiseLoopWrapper):
1566
+ """
1567
+ Denoising loop for the distilled Krea 2 Turbo checkpoint with reference conditioning: the clean reference
1568
+ tokens are spliced into the sequence and sliced back off the prediction. No classifier-free guidance.
1569
+ """
1570
+
1571
+ model_name = "krea2"
1572
+ block_classes = [Krea2LoopBeforeDenoiser, Krea2TurboReferenceLoopDenoiser, Krea2LoopAfterDenoiser]
1573
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
1574
+
1575
+ @property
1576
+ def description(self) -> str:
1577
+ return (
1578
+ "Denoising loop for the distilled Krea 2 Turbo checkpoint with reference conditioning, without "
1579
+ "classifier-free guidance."
1580
+ )
1581
+
1582
+
1583
+ class Krea2TurboVisionReferenceLoopDenoiser(Krea2TurboReferenceLoopDenoiser):
1584
+ reference_mode = REFERENCE_MODE_OFF
1585
+
1586
+
1587
+ class Krea2TurboAppendReferenceLoopDenoiser(Krea2TurboReferenceLoopDenoiser):
1588
+ reference_mode = REFERENCE_MODE_APPEND
1589
+
1590
+
1591
+ class Krea2TurboPrependReferenceLoopDenoiser(Krea2TurboReferenceLoopDenoiser):
1592
+ reference_mode = REFERENCE_MODE_PREPEND
1593
+
1594
+
1595
+ def _turbo_denoise_step(name, denoiser, summary):
1596
+ """Build a guidance-free denoise loop for one mode."""
1597
+ return type(
1598
+ name,
1599
+ (Krea2DenoiseLoopWrapper,),
1600
+ {
1601
+ "__doc__": summary,
1602
+ "model_name": "krea2",
1603
+ "block_classes": [Krea2LoopBeforeDenoiser, denoiser, Krea2LoopAfterDenoiser],
1604
+ "block_names": ["before_denoiser", "denoiser", "after_denoiser"],
1605
+ "description": property(lambda self, _s=summary: _s),
1606
+ },
1607
+ )
1608
+
1609
+
1610
+ Krea2TurboVisionReferenceDenoiseStep = _turbo_denoise_step(
1611
+ "Krea2TurboVisionReferenceDenoiseStep",
1612
+ Krea2TurboVisionReferenceLoopDenoiser,
1613
+ "Guidance-free denoising loop for the vision-only mode on the distilled checkpoint.",
1614
+ )
1615
+ Krea2TurboAppendReferenceDenoiseStep = _turbo_denoise_step(
1616
+ "Krea2TurboAppendReferenceDenoiseStep",
1617
+ Krea2TurboAppendReferenceLoopDenoiser,
1618
+ "Guidance-free denoising loop for the Ostris append mode on the distilled checkpoint.",
1619
+ )
1620
+ Krea2TurboPrependReferenceDenoiseStep = _turbo_denoise_step(
1621
+ "Krea2TurboPrependReferenceDenoiseStep",
1622
+ Krea2TurboPrependReferenceLoopDenoiser,
1623
+ "Guidance-free denoising loop for the Identity-Edit prepend mode on the distilled checkpoint.",
1624
+ )
1625
+
1626
+
1627
+ # =====================================================================================================
1628
+ # Workflows: one flat blockset per mode, per checkpoint.
1629
+ # =====================================================================================================
1630
+
1631
+
1632
+ def _workflow(name, images, encoder, timesteps, position_ids, denoise, latents, summary):
1633
+ core = InsertableDict(
1634
+ [("input", Krea2ReferenceTextInputsStep())]
1635
+ + ([("reference_latents", Krea2ReferenceLatentsStep())] if latents else [])
1636
+ + [
1637
+ ("prepare_latents", Krea2PrepareLatentsStep()),
1638
+ ("set_timesteps", timesteps()),
1639
+ ("prepare_position_ids", position_ids()),
1640
+ ("denoise", denoise()),
1641
+ ]
1642
+ )
1643
+ core_cls = type(
1644
+ f"{name}CoreDenoiseStep",
1645
+ (SequentialPipelineBlocks,),
1646
+ {
1647
+ "__doc__": f"Core denoising workflow: {summary}",
1648
+ "model_name": "krea2",
1649
+ "block_classes": list(core.values()),
1650
+ "block_names": list(core.keys()),
1651
+ "description": property(lambda self, _s=summary: f"Core denoising workflow: {_s}"),
1652
+ "outputs": property(
1653
+ lambda self: [
1654
+ OutputParam.template(
1655
+ "latents", description="The denoised packed latents (B, image_seq_len, in_channels)."
1656
+ )
1657
+ ]
1658
+ ),
1659
+ },
1660
+ )
1661
+ return type(
1662
+ name,
1663
+ (SequentialPipelineBlocks,),
1664
+ {
1665
+ "__doc__": summary,
1666
+ "model_name": "krea2",
1667
+ "block_classes": [images, encoder, core_cls, Krea2DecodeStep],
1668
+ "block_names": ["reference_images", "text_encoder", "denoise", "decode"],
1669
+ "description": property(lambda self, _s=summary: _s),
1670
+ "outputs": property(lambda self: [OutputParam.template("images")]),
1671
+ },
1672
+ )
1673
+
1674
+
1675
+ Krea2VisionReferenceWorkflow = _workflow(
1676
+ "Krea2VisionReferenceWorkflow",
1677
+ Krea2VisionReferenceImagesStep,
1678
+ Krea2VisionReferenceTextEncoderStep,
1679
+ Krea2SetTimestepsStep,
1680
+ Krea2VisionReferencePositionIdsStep,
1681
+ Krea2VisionReferenceDenoiseStep,
1682
+ latents=False,
1683
+ summary="vision-only references on the stock checkpoint, no LoRA needed.",
1684
+ )
1685
+ Krea2AppendReferenceWorkflow = _workflow(
1686
+ "Krea2AppendReferenceWorkflow",
1687
+ Krea2AppendReferenceImagesStep,
1688
+ Krea2AppendReferenceTextEncoderStep,
1689
+ Krea2SetTimestepsStep,
1690
+ Krea2AppendReferencePositionIdsStep,
1691
+ Krea2AppendReferenceDenoiseStep,
1692
+ latents=True,
1693
+ summary="vision plus clean VAE reference tokens after the target, for the Ostris edit LoRA.",
1694
+ )
1695
+ Krea2PrependReferenceWorkflow = _workflow(
1696
+ "Krea2PrependReferenceWorkflow",
1697
+ Krea2PrependReferenceImagesStep,
1698
+ Krea2PrependReferenceTextEncoderStep,
1699
+ Krea2SetTimestepsStep,
1700
+ Krea2PrependReferencePositionIdsStep,
1701
+ Krea2PrependReferenceDenoiseStep,
1702
+ latents=True,
1703
+ summary="vision plus the clean VAE source before the target, for the Identity-Edit LoRA.",
1704
+ )
1705
+
1706
+ Krea2TurboVisionReferenceWorkflow = _workflow(
1707
+ "Krea2TurboVisionReferenceWorkflow",
1708
+ Krea2VisionReferenceImagesStep,
1709
+ Krea2TurboVisionReferenceTextEncoderStep,
1710
+ Krea2TurboSetTimestepsStep,
1711
+ Krea2VisionReferencePositionIdsStep,
1712
+ Krea2TurboVisionReferenceDenoiseStep,
1713
+ latents=False,
1714
+ summary="vision-only references on the distilled checkpoint.",
1715
+ )
1716
+ Krea2TurboAppendReferenceWorkflow = _workflow(
1717
+ "Krea2TurboAppendReferenceWorkflow",
1718
+ Krea2AppendReferenceImagesStep,
1719
+ Krea2TurboAppendReferenceTextEncoderStep,
1720
+ Krea2TurboSetTimestepsStep,
1721
+ Krea2AppendReferencePositionIdsStep,
1722
+ Krea2TurboAppendReferenceDenoiseStep,
1723
+ latents=True,
1724
+ summary="vision plus clean VAE reference tokens after the target, for the Ostris Turbo edit LoRA.",
1725
+ )
1726
+ Krea2TurboPrependReferenceWorkflow = _workflow(
1727
+ "Krea2TurboPrependReferenceWorkflow",
1728
+ Krea2PrependReferenceImagesStep,
1729
+ Krea2TurboPrependReferenceTextEncoderStep,
1730
+ Krea2TurboSetTimestepsStep,
1731
+ Krea2PrependReferencePositionIdsStep,
1732
+ Krea2TurboPrependReferenceDenoiseStep,
1733
+ latents=True,
1734
+ summary="vision plus the clean VAE source before the target, on the distilled checkpoint.",
1735
+ )
1736
+
1737
+
1738
+ # auto_docstring
1739
+ class Krea2ReferenceAutoBlocks(ConditionalPipelineBlocks):
1740
+ """
1741
+ Modular pipeline for Krea 2 with reference-image conditioning. `reference_mode` picks the workflow:
1742
+ `"off"` (vision path only, stock checkpoint), `"append"` (Ostris edit LoRA) or `"prepend"` (Identity-Edit
1743
+ LoRA). With no `reference_images` this is plain text-to-image.
1744
+ """
1745
+
1746
+ model_name = "krea2"
1747
+ block_classes = [
1748
+ Krea2VisionReferenceWorkflow,
1749
+ Krea2AppendReferenceWorkflow,
1750
+ Krea2PrependReferenceWorkflow,
1751
+ ]
1752
+ block_names = [REFERENCE_MODE_OFF, REFERENCE_MODE_APPEND, REFERENCE_MODE_PREPEND]
1753
+ block_trigger_inputs = ["reference_mode"]
1754
+ default_block_name = REFERENCE_MODE_OFF
1755
+
1756
+ @property
1757
+ def description(self) -> str:
1758
+ return (
1759
+ "Krea 2 with reference-image conditioning: `reference_mode` selects the vision-only path, the "
1760
+ "Ostris append path or the Identity-Edit prepend path."
1761
+ )
1762
+
1763
+ def select_block(self, **kwargs) -> str:
1764
+ return _coerce_reference_mode(kwargs.get("reference_mode"))
1765
+
1766
+
1767
+ # auto_docstring
1768
+ class Krea2TurboReferenceAutoBlocks(Krea2ReferenceAutoBlocks):
1769
+ """
1770
+ Modular pipeline for the distilled Krea 2 Turbo checkpoint with reference-image conditioning. The same three
1771
+ modes on the distilled schedule and without classifier-free guidance, so it takes no negative prompt and
1772
+ carries no guider.
1773
+ """
1774
+
1775
+ model_name = "krea2"
1776
+ block_classes = [
1777
+ Krea2TurboVisionReferenceWorkflow,
1778
+ Krea2TurboAppendReferenceWorkflow,
1779
+ Krea2TurboPrependReferenceWorkflow,
1780
+ ]
1781
+ block_names = [REFERENCE_MODE_OFF, REFERENCE_MODE_APPEND, REFERENCE_MODE_PREPEND]
1782
+ block_trigger_inputs = ["reference_mode"]
1783
+ default_block_name = REFERENCE_MODE_OFF
1784
+
1785
+ @property
1786
+ def description(self) -> str:
1787
+ return (
1788
+ "Krea 2 Turbo with reference-image conditioning: the distilled schedule, no CFG, and the same "
1789
+ "three reference modes."
1790
+ )
krea2_vision_attention.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Restrict Qwen3-VL's vision self-attention to the painted region of a Krea 2 reference image.
15
+
16
+ Zeroing the vision tower's OUTPUT tokens cannot remove content from a reference. `Qwen3VLVisionModel`
17
+ runs its full depth of self-attention over every patch of the image (`cu_seqlens` only separates one
18
+ image from the next), so every output token is a contextual summary of the whole picture: dropping the
19
+ tokens that cover a subject deletes its dedicated slots and nothing else. Removal requires masking
20
+ inside the attention, so kept patches never attend to unpainted patches as keys.
21
+
22
+ `Qwen3VLVisionAttention.forward` is patched to accept a per-image key mask, active only inside
23
+ `vision_key_masks(...)`. With no active mask it defers to the original forward, so unmasked encodes are
24
+ unchanged.
25
+
26
+ Two constraints that are silent when broken:
27
+
28
+ * The mask lives at **pre-merge patch resolution** (`grid_h x grid_w`) and the flattened patch sequence
29
+ is **not row-major**. The image processor groups patches by `merge_size x merge_size` window
30
+ (`permute(0, 2, 5, 3, 6, 1, 4, 7)` over `(batch, channel, gh/m, m, ps, gw/m, m, ps)`), giving
31
+ sequence order `(h_block, w_block, m_row, m_col)`. `patch_keep_vector` reproduces that order.
32
+ * A merged token whose `merge_size x merge_size` window straddles the mask boundary still mixes
33
+ unpainted patches, so about a token of contamination survives at the edge.
34
+ """
35
+
36
+ import inspect
37
+ from contextlib import contextmanager
38
+ from contextvars import ContextVar
39
+
40
+ import numpy as np
41
+ import torch
42
+
43
+ from diffusers.utils import logging
44
+
45
+
46
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
47
+
48
+ # A ContextVar rather than a global so a nested or concurrent encode cannot inherit another call's masks.
49
+ _ACTIVE_KEEPS: ContextVar[list | None] = ContextVar("krea2_vision_keeps", default=None)
50
+
51
+ _ORIGINAL_FORWARD = None
52
+ _PATCHED = False
53
+
54
+
55
+ def patch_keep_vector(keep_grid: np.ndarray, merge_size: int, temporal: int = 1) -> np.ndarray:
56
+ """Reorder a `(grid_h, grid_w)` keep grid into the processor's flattened patch order.
57
+
58
+ Sequence order is `(h_block, w_block, m_row, m_col)`, so a row-major flatten is wrong wherever
59
+ `merge_size > 1`.
60
+ """
61
+ grid_height, grid_width = keep_grid.shape
62
+ blocked = keep_grid.reshape(grid_height // merge_size, merge_size, grid_width // merge_size, merge_size)
63
+ ordered = blocked.transpose(0, 2, 1, 3).reshape(-1)
64
+ return np.tile(ordered, temporal) if temporal > 1 else ordered
65
+
66
+
67
+ def build_patch_keeps(masks, image_grid_thw, merge_size: int, mask_to_grid) -> list:
68
+ """One patch keep vector per row of `image_grid_thw` (bool, processor patch order), or `None` per slot.
69
+
70
+ `mask_to_grid(mask, grid_height, grid_width)` rasterizes a mask onto a grid; it is injected so this
71
+ module carries no mask-format dependency.
72
+ """
73
+ if image_grid_thw is None:
74
+ return []
75
+ masks = list(masks or [])
76
+ keeps = []
77
+ for i in range(int(image_grid_thw.shape[0])):
78
+ mask = masks[i] if i < len(masks) else None
79
+ t, h, w = (int(x) for x in image_grid_thw[i].tolist())
80
+ grid = mask_to_grid(mask, h, w) if mask is not None else None
81
+ if grid is None:
82
+ keeps.append(None)
83
+ continue
84
+ keeps.append(torch.from_numpy(patch_keep_vector(grid, merge_size, t)).bool())
85
+ return keeps
86
+
87
+
88
+ @contextmanager
89
+ def vision_key_masks(keeps):
90
+ """Activate per-image patch key masks for the vision attention inside this block."""
91
+ if not keeps or all(keep is None for keep in keeps):
92
+ yield False
93
+ return
94
+ if not ensure_vision_attention_patched():
95
+ logger.warning(
96
+ "Qwen3-VL vision-attention patch unavailable; reference masks will only zero output tokens, "
97
+ "which cannot remove content from a reference."
98
+ )
99
+ yield False
100
+ return
101
+ token = _ACTIVE_KEEPS.set(list(keeps))
102
+ try:
103
+ yield True
104
+ finally:
105
+ _ACTIVE_KEEPS.reset(token)
106
+
107
+
108
+ def _key_bias(keep: torch.Tensor, seq_len: int, device, dtype):
109
+ """Additive `(1, 1, 1, seq_len)` bias: 0 for kept keys, dtype-min for dropped ones.
110
+
111
+ Additive rather than a bool mask because the eager attention fallback adds the mask to the scores.
112
+ Only keys are masked, never queries, so no row can be fully masked and softmax cannot produce NaN.
113
+ """
114
+ if int(keep.numel()) != seq_len:
115
+ logger.warning(
116
+ f"Vision key mask length {int(keep.numel())} != patch sequence {seq_len}; skipping it for this image."
117
+ )
118
+ return None
119
+ bias = torch.zeros(1, 1, 1, seq_len, device=device, dtype=dtype)
120
+ bias.masked_fill_(~keep.to(device).view(1, 1, 1, seq_len), torch.finfo(dtype).min)
121
+ return bias
122
+
123
+
124
+ def _forward_with_key_mask(self, hidden_states, cu_seqlens, position_embeddings=None, **kwargs):
125
+ keeps = _ACTIVE_KEEPS.get()
126
+ if not keeps:
127
+ return _ORIGINAL_FORWARD(
128
+ self,
129
+ hidden_states,
130
+ cu_seqlens,
131
+ position_embeddings=position_embeddings,
132
+ **kwargs,
133
+ )
134
+
135
+ from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
136
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import (
137
+ apply_rotary_pos_emb_vision,
138
+ eager_attention_forward,
139
+ )
140
+
141
+ seq_length = hidden_states.shape[0]
142
+ query_states, key_states, value_states = (
143
+ self.qkv(hidden_states).reshape(seq_length, 3, self.num_heads, -1).permute(1, 0, 2, 3).unbind(0)
144
+ )
145
+ cos, sin = position_embeddings
146
+ query_states, key_states = apply_rotary_pos_emb_vision(query_states, key_states, cos, sin)
147
+
148
+ query_states = query_states.transpose(0, 1).unsqueeze(0)
149
+ key_states = key_states.transpose(0, 1).unsqueeze(0)
150
+ value_states = value_states.transpose(0, 1).unsqueeze(0)
151
+
152
+ attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface(
153
+ self.config._attn_implementation, eager_attention_forward
154
+ )
155
+
156
+ # Always the per-image split path, never the flash `cu_seqlens` path: a key mask is per image, and
157
+ # varlen flash attention takes no mask. This is the same partition upstream uses for SDPA.
158
+ lengths = (cu_seqlens[1:] - cu_seqlens[:-1]).tolist()
159
+ splits = [torch.split(tensor, lengths, dim=2) for tensor in (query_states, key_states, value_states)]
160
+
161
+ attn_outputs = []
162
+ for i, (q, k, v) in enumerate(zip(*splits)):
163
+ keep = keeps[i] if i < len(keeps) else None
164
+ bias = None if keep is None else _key_bias(keep, q.shape[2], q.device, q.dtype)
165
+ attn_outputs.append(
166
+ attention_interface(
167
+ self,
168
+ q,
169
+ k,
170
+ v,
171
+ attention_mask=bias,
172
+ scaling=self.scaling,
173
+ dropout=0.0,
174
+ is_causal=False,
175
+ **kwargs,
176
+ )[0]
177
+ )
178
+
179
+ attn_output = torch.cat(attn_outputs, dim=1)
180
+ attn_output = attn_output.reshape(seq_length, -1).contiguous()
181
+ return self.proj(attn_output)
182
+
183
+
184
+ def ensure_vision_attention_patched() -> bool:
185
+ """Patch `Qwen3VLVisionAttention.forward` once. Returns False if the shape it relies on changed."""
186
+ global _ORIGINAL_FORWARD, _PATCHED
187
+ if _PATCHED:
188
+ return True
189
+ try:
190
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import (
191
+ Qwen3VLVisionAttention,
192
+ )
193
+ except ImportError:
194
+ return False
195
+
196
+ # `_forward_with_key_mask` reproduces this signature and these attributes. If transformers changes
197
+ # either, skip patching and degrade to output-only masking rather than crash.
198
+ forward = getattr(Qwen3VLVisionAttention, "forward", None)
199
+ if forward is None:
200
+ return False
201
+ try:
202
+ params = inspect.signature(forward).parameters
203
+ init_src = inspect.getsource(Qwen3VLVisionAttention.__init__)
204
+ except (TypeError, ValueError, OSError):
205
+ return False
206
+ if not {"hidden_states", "cu_seqlens", "position_embeddings"}.issubset(params):
207
+ return False
208
+ if not all(f"self.{attr}" in init_src for attr in ("qkv", "proj", "num_heads", "scaling", "config")):
209
+ return False
210
+
211
+ _ORIGINAL_FORWARD = forward
212
+ Qwen3VLVisionAttention.forward = _forward_with_key_mask
213
+ _PATCHED = True
214
+ logger.debug("Patched Qwen3VLVisionAttention.forward for reference key masking.")
215
+ return True
modular_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "Krea2TurboReferenceAutoBlocks",
3
+ "_diffusers_version": "0.41.0.dev0",
4
+ "auto_map": {
5
+ "ModularPipelineBlocks": "krea2_reference.Krea2TurboReferenceAutoBlocks"
6
+ },
7
+ "requirements": {
8
+ "sdnq": ">=0.2.0"
9
+ }
10
+ }
modular_model_index.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_blocks_class_name": "Krea2TurboReferenceAutoBlocks",
3
+ "_class_name": "Krea2ModularPipeline",
4
+ "_diffusers_version": "0.41.0.dev0",
5
+ "scheduler": [
6
+ "diffusers",
7
+ "FlowMatchEulerDiscreteScheduler",
8
+ {
9
+ "pretrained_model_name_or_path": "OzzyGT/Krea_2_Turbo_sdnq_dynamic_8bit",
10
+ "revision": null,
11
+ "subfolder": "scheduler",
12
+ "type_hint": [
13
+ "diffusers",
14
+ "FlowMatchEulerDiscreteScheduler"
15
+ ],
16
+ "variant": null
17
+ }
18
+ ],
19
+ "text_encoder": [
20
+ "transformers",
21
+ "Qwen3VLModel",
22
+ {
23
+ "pretrained_model_name_or_path": "OzzyGT/Krea_2_Turbo_sdnq_dynamic_8bit",
24
+ "revision": null,
25
+ "subfolder": "text_encoder",
26
+ "type_hint": [
27
+ "transformers",
28
+ "Qwen3VLModel"
29
+ ],
30
+ "variant": null
31
+ }
32
+ ],
33
+ "tokenizer": [
34
+ "transformers",
35
+ "Qwen2Tokenizer",
36
+ {
37
+ "pretrained_model_name_or_path": "OzzyGT/Krea_2_Turbo_sdnq_dynamic_8bit",
38
+ "revision": null,
39
+ "subfolder": "tokenizer",
40
+ "type_hint": [
41
+ "transformers",
42
+ "Qwen2Tokenizer"
43
+ ],
44
+ "variant": null
45
+ }
46
+ ],
47
+ "transformer": [
48
+ "diffusers",
49
+ "Krea2Transformer2DModel",
50
+ {
51
+ "pretrained_model_name_or_path": "OzzyGT/Krea_2_Turbo_sdnq_dynamic_8bit",
52
+ "revision": null,
53
+ "subfolder": "transformer",
54
+ "type_hint": [
55
+ "diffusers",
56
+ "Krea2Transformer2DModel"
57
+ ],
58
+ "variant": null
59
+ }
60
+ ],
61
+ "vae": [
62
+ "diffusers",
63
+ "AutoencoderKLQwenImage",
64
+ {
65
+ "pretrained_model_name_or_path": "OzzyGT/Krea_2_Turbo_sdnq_dynamic_8bit",
66
+ "revision": null,
67
+ "subfolder": "vae",
68
+ "type_hint": [
69
+ "diffusers",
70
+ "AutoencoderKLQwenImage"
71
+ ],
72
+ "variant": null
73
+ }
74
+ ]
75
+ }