yijunwang2 commited on
Commit
ff0a37b
·
verified ·
1 Parent(s): 606fe5d

Upload private Krea 2 Outpaint release candidate

Browse files
Files changed (10) hide show
  1. .gitattributes +1 -0
  2. LICENSE.pdf +3 -0
  3. NOTICE +15 -0
  4. PIPELINE_LICENSE +202 -0
  5. README.md +145 -0
  6. SHA256SUMS +8 -0
  7. example.py +68 -0
  8. krea2_outpaint_rank32.safetensors +3 -0
  9. outpaint.py +150 -0
  10. pipeline.py +1522 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ LICENSE.pdf filter=lfs diff=lfs merge=lfs -text
LICENSE.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b82a2805162bde714a4eb27b9063c4fc3345d08a30be055134a6160e5430ba74
3
+ size 137711
NOTICE ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Krea 2 Registered Outpaint
2
+
3
+ The model weights in this repository are derived from Krea 2 and are subject to
4
+ the Krea 2 Community License included as LICENSE.pdf.
5
+
6
+ The included pipeline is derived from ostris/Krea2OstrisEdit and adds registered
7
+ reference placement through destination-relative rotary coordinates. Pipeline
8
+ and helper code are distributed under the Apache License 2.0.
9
+
10
+ Krea 2: https://huggingface.co/krea/Krea-2-Turbo
11
+ Upstream edit pipeline: https://huggingface.co/ostris/Krea2OstrisEdit
12
+
13
+ This is an unofficial community release and is not endorsed by Krea or Ostris.
14
+ Training data is not included.
15
+
PIPELINE_LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: krea-2-community-license
4
+ license_link: https://krea.ai/krea-2-licensing
5
+ base_model: krea/Krea-2-Turbo
6
+ pipeline_tag: image-to-image
7
+ tags:
8
+ - krea-2
9
+ - lora
10
+ - outpainting
11
+ - image-editing
12
+ - diffusers
13
+ ---
14
+
15
+ # Krea 2 Registered Outpaint
16
+
17
+ > Draft release candidate. This repository is private while its documentation,
18
+ > license files, and clean-download inference path are reviewed.
19
+
20
+ This rank-32 LoRA extends an image into a larger canvas while preserving its
21
+ known pixels and registering the source image at an explicit canvas location.
22
+ It was trained against Krea 2 Raw and is intended for distilled 8-step inference
23
+ with Krea 2 Turbo.
24
+
25
+ Unlike a plain image-reference adapter, the source latent tokens receive rotary
26
+ coordinates derived from their destination bounding box. The denoiser can
27
+ therefore reason about where the known image belongs in the target canvas. The
28
+ included pipeline is based on
29
+ [ostris/Krea2OstrisEdit](https://huggingface.co/ostris/Krea2OstrisEdit) and adds
30
+ the registered `reference_placements` input used by this adapter.
31
+
32
+ ## Files
33
+
34
+ ```text
35
+ krea2_outpaint_rank32.safetensors LoRA weights
36
+ pipeline.py Krea 2 edit pipeline with registered references
37
+ outpaint.py Canvas preparation and seam compositing helpers
38
+ example.py Minimal local example
39
+ LICENSE.pdf Krea 2 Community License
40
+ PIPELINE_LICENSE Apache-2.0 license for pipeline/helper code
41
+ NOTICE attribution and derivative-work notice
42
+ SHA256SUMS release artifact hashes
43
+ ```
44
+
45
+ ## Input contract
46
+
47
+ The model takes:
48
+
49
+ - one RGB source image;
50
+ - an output canvas whose width and height are multiples of 16;
51
+ - a source bounding box `(x0, y0, x1, y1)` in output pixels;
52
+ - a prompt describing the complete output image.
53
+
54
+ The bounding box must stay inside the canvas and preserve the source aspect
55
+ ratio. The helper resizes the source to that exact box. Conditioning is encoded
56
+ at a maximum edge of 384 pixels; the original resized pixels are restored after
57
+ generation with a 32-pixel inward feather.
58
+
59
+ For a single pass, the source box should span the full canvas width or height.
60
+ `outpaint.py` also provides a two-pass plan for arbitrary interior boxes. It
61
+ chooses the smaller intermediate blank region first.
62
+
63
+ ## Recommended settings
64
+
65
+ | Setting | Value |
66
+ |---|---:|
67
+ | Base | `krea/Krea-2-Turbo` |
68
+ | Steps | 8 |
69
+ | Guidance scale | 0.0 |
70
+ | LoRA scale | 1.0 |
71
+ | Reference maximum edge | 384 px |
72
+ | Seam feather | 32 px |
73
+ | Reference VLM encoding | disabled |
74
+ | Reference K/V cache | enabled |
75
+
76
+ The adapter is an AI-Toolkit LoRA and uses isolated reference attention, so
77
+ `kv_cache=True` and `encode_reference_in_prompt=False` are part of the model
78
+ contract rather than optional speed tweaks.
79
+
80
+ ## Usage
81
+
82
+ Install a current PyTorch build for your GPU, then install the dependencies:
83
+
84
+ ```bash
85
+ pip install diffusers transformers accelerate safetensors huggingface_hub pillow
86
+ ```
87
+
88
+ Run the included example after downloading this repository:
89
+
90
+ ```bash
91
+ python example.py \
92
+ --source input.png \
93
+ --output output.png \
94
+ --width 1024 \
95
+ --height 1536 \
96
+ --bbox 0 0 1024 1024 \
97
+ --prompt "a polished digital illustration, full composition"
98
+ ```
99
+
100
+ The example defaults to BF16. Quantized Krea 2 runtimes may also load the LoRA,
101
+ but quantization setup is runtime-specific and is not bundled here.
102
+
103
+ ## How it works
104
+
105
+ The noisy target follows normal Krea 2 flow-matching inference. The clean source
106
+ is VAE-encoded as a separate token sequence. For a normalized source box
107
+ `(x0, y0, x1, y1)`, source token coordinates are mapped into the target latent
108
+ grid before rotary position encoding. Reference K/V is computed once and reused
109
+ through all denoising steps. Finally, exact source pixels are composited back
110
+ into the known region with a short inward feather to retain detail while hiding
111
+ small decode differences at the boundary.
112
+
113
+ ## Limitations
114
+
115
+ - Very large missing regions can introduce a second subject or unrelated scene.
116
+ - Interior placement uses two generations and can accumulate changes between
117
+ passes.
118
+ - The model handles rectangular outpainting, not arbitrary inpainting masks.
119
+ - Prompt wording and source composition affect how aggressively content is
120
+ extended.
121
+ - The 384-pixel conditioning budget prioritizes speed and structure over exact
122
+ recovery of tiny source details; final compositing preserves the known pixels.
123
+ - BF16 clean-download inference remains a release-blocking verification item for
124
+ this draft.
125
+
126
+ ## Training summary
127
+
128
+ The adapter was trained at rank/alpha 32/32 on rectangular crops registered to
129
+ their location in complete target images. Targets were at most 1024 pixels on
130
+ their longest edge and source conditions at most 384 pixels. The final candidate
131
+ is the 12,000-step checkpoint. Training data and training infrastructure are not
132
+ distributed.
133
+
134
+ In the tested INT8 ConvRot Krea 2 Turbo runtime on an RTX 5090, one 8-step pass
135
+ took approximately 3.6-4.2 seconds at the evaluated native resolutions, with
136
+ prompt encoding around 0.1 seconds. These measurements are implementation- and
137
+ hardware-specific and are not BF16 benchmark claims.
138
+
139
+ ## License
140
+
141
+ The LoRA weights are a derivative of Krea 2 and are released under the Krea 2
142
+ Community License in `LICENSE.pdf`. Pipeline and helper code are distributed
143
+ under Apache-2.0 in `PIPELINE_LICENSE`. This is an unofficial community release
144
+ and is not endorsed by Krea or Ostris.
145
+
SHA256SUMS ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ b82a2805162bde714a4eb27b9063c4fc3345d08a30be055134a6160e5430ba74 LICENSE.pdf
2
+ 5d23d01efae1fde8eccd4f00215386860c89ef5071085f1e7b311bda1baafeb1 NOTICE
3
+ cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 PIPELINE_LICENSE
4
+ f76772b7e9be831d8b761a3394c1731182ac9b9a4dc8a628376edca3bfbde117 README.md
5
+ 39a7e308656302da1cc2c26130ff512f8931197033743913d7bfaecac2c6ff15 example.py
6
+ 1de7c106ae978de21433152bdb4ba6b09118b33be81b7ee4851420f0013a7e76 krea2_outpaint_rank32.safetensors
7
+ cb200ee7b4d12eb5cc1f663a661b7b7902bc955dc3d6286447a2ee77fbedc7d7 outpaint.py
8
+ 877de17e0a9f7e42700d345b84a2ae9a3a3e559fdd5a69d4fb66a1db35f4449f pipeline.py
example.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import torch
7
+ from diffusers import DiffusionPipeline
8
+ from PIL import Image
9
+
10
+ from outpaint import composite, prepare_source
11
+
12
+
13
+ REPO_ID = "yijunwang2/krea2-outpaint"
14
+ WEIGHT_NAME = "krea2_outpaint_rank32.safetensors"
15
+
16
+
17
+ def parse_args() -> argparse.Namespace:
18
+ parser = argparse.ArgumentParser(description="Run registered Krea 2 outpainting")
19
+ parser.add_argument("--source", type=Path, required=True)
20
+ parser.add_argument("--output", type=Path, required=True)
21
+ parser.add_argument("--width", type=int, required=True)
22
+ parser.add_argument("--height", type=int, required=True)
23
+ parser.add_argument("--bbox", type=int, nargs=4, metavar=("X0", "Y0", "X1", "Y1"), required=True)
24
+ parser.add_argument("--prompt", required=True)
25
+ parser.add_argument("--steps", type=int, default=8)
26
+ parser.add_argument("--seed", type=int, default=42)
27
+ parser.add_argument("--lora-scale", type=float, default=1.0)
28
+ return parser.parse_args()
29
+
30
+
31
+ def main() -> None:
32
+ args = parse_args()
33
+ prepared = prepare_source(
34
+ Image.open(args.source),
35
+ (args.width, args.height),
36
+ tuple(args.bbox),
37
+ )
38
+
39
+ pipe = DiffusionPipeline.from_pretrained(
40
+ "krea/Krea-2-Turbo",
41
+ custom_pipeline=REPO_ID,
42
+ torch_dtype=torch.bfloat16,
43
+ ).to("cuda")
44
+ pipe.load_lora_weights(REPO_ID, weight_name=WEIGHT_NAME, adapter_name="outpaint")
45
+ pipe.set_adapters(["outpaint"], adapter_weights=[args.lora_scale])
46
+
47
+ generator = torch.Generator(device="cuda").manual_seed(args.seed)
48
+ generated = pipe(
49
+ prompt=args.prompt,
50
+ image=prepared.condition,
51
+ width=args.width,
52
+ height=args.height,
53
+ num_inference_steps=args.steps,
54
+ guidance_scale=0.0,
55
+ generator=generator,
56
+ reference_max_pixels=384 * 384,
57
+ reference_placements=[{"bbox_normalized": prepared.bbox_normalized}],
58
+ encode_reference_in_prompt=False,
59
+ kv_cache=True,
60
+ ).images[0]
61
+
62
+ args.output.parent.mkdir(parents=True, exist_ok=True)
63
+ composite(generated, prepared).save(args.output)
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
68
+
krea2_outpaint_rank32.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1de7c106ae978de21433152bdb4ba6b09118b33be81b7ee4851420f0013a7e76
3
+ size 228587736
outpaint.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+
6
+ import numpy as np
7
+ from PIL import Image
8
+
9
+
10
+ SOURCE_MAX_EDGE = 384
11
+ SEAM_PX = 32
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class RegisteredSource:
16
+ condition: Image.Image
17
+ placed_source: Image.Image
18
+ canvas_size: tuple[int, int]
19
+ bbox: tuple[int, int, int, int]
20
+ seam_px: int = SEAM_PX
21
+
22
+ @property
23
+ def bbox_normalized(self) -> list[float]:
24
+ width, height = self.canvas_size
25
+ x0, y0, x1, y1 = self.bbox
26
+ return [x0 / width, y0 / height, x1 / width, y1 / height]
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class PassPlan:
31
+ axis: str
32
+ intermediate_size: tuple[int, int] | None
33
+ first_bbox: tuple[int, int, int, int]
34
+ second_bbox: tuple[int, int, int, int] | None
35
+
36
+ @property
37
+ def pass_count(self) -> int:
38
+ return 1 if self.intermediate_size is None else 2
39
+
40
+
41
+ def _resize_max_edge(image: Image.Image, max_edge: int) -> Image.Image:
42
+ if max(image.size) <= max_edge:
43
+ return image.copy()
44
+ scale = max_edge / max(image.size)
45
+ size = (max(1, round(image.width * scale)), max(1, round(image.height * scale)))
46
+ return image.resize(size, Image.Resampling.LANCZOS)
47
+
48
+
49
+ def prepare_source(
50
+ source: Image.Image,
51
+ canvas_size: tuple[int, int],
52
+ bbox: tuple[int, int, int, int],
53
+ *,
54
+ source_max_edge: int = SOURCE_MAX_EDGE,
55
+ seam_px: int = SEAM_PX,
56
+ ) -> RegisteredSource:
57
+ width, height = canvas_size
58
+ x0, y0, x1, y1 = bbox
59
+ if width < 16 or height < 16 or width % 16 or height % 16:
60
+ raise ValueError("Canvas dimensions must be positive multiples of 16")
61
+ if not (0 <= x0 < x1 <= width and 0 <= y0 < y1 <= height):
62
+ raise ValueError(f"Source bbox is outside the canvas: {bbox}")
63
+
64
+ source = source.convert("RGB")
65
+ box_width, box_height = x1 - x0, y1 - y0
66
+ source_ratio = source.width / source.height
67
+ box_ratio = box_width / box_height
68
+ tolerance = max(0.025, 2.0 / min(box_width, box_height))
69
+ if abs(box_ratio / source_ratio - 1.0) > tolerance:
70
+ raise ValueError("Source bbox must preserve the source image aspect ratio")
71
+
72
+ placed = source.resize((box_width, box_height), Image.Resampling.LANCZOS)
73
+ return RegisteredSource(
74
+ condition=_resize_max_edge(placed, source_max_edge),
75
+ placed_source=placed,
76
+ canvas_size=canvas_size,
77
+ bbox=bbox,
78
+ seam_px=seam_px,
79
+ )
80
+
81
+
82
+ def _align_up(value: int, alignment: int = 16) -> int:
83
+ return int(math.ceil(value / alignment) * alignment)
84
+
85
+
86
+ def plan_passes(prepared: RegisteredSource) -> PassPlan:
87
+ width, height = prepared.canvas_size
88
+ x0, y0, x1, y1 = prepared.bbox
89
+ source_width, source_height = x1 - x0, y1 - y0
90
+ if source_width == width or source_height == height:
91
+ return PassPlan("direct", None, prepared.bbox, None)
92
+
93
+ candidates: list[tuple[int, PassPlan]] = []
94
+ intermediate_height = _align_up(source_height)
95
+ if intermediate_height < height:
96
+ intermediate_y = max(
97
+ 0,
98
+ min(height - intermediate_height, y0 - (intermediate_height - source_height) // 2),
99
+ )
100
+ local_y = y0 - intermediate_y
101
+ candidates.append(
102
+ (
103
+ width * intermediate_height,
104
+ PassPlan(
105
+ "horizontal_first",
106
+ (width, intermediate_height),
107
+ (x0, local_y, x1, local_y + source_height),
108
+ (0, intermediate_y, width, intermediate_y + intermediate_height),
109
+ ),
110
+ )
111
+ )
112
+
113
+ intermediate_width = _align_up(source_width)
114
+ if intermediate_width < width:
115
+ intermediate_x = max(
116
+ 0,
117
+ min(width - intermediate_width, x0 - (intermediate_width - source_width) // 2),
118
+ )
119
+ local_x = x0 - intermediate_x
120
+ candidates.append(
121
+ (
122
+ intermediate_width * height,
123
+ PassPlan(
124
+ "vertical_first",
125
+ (intermediate_width, height),
126
+ (local_x, y0, local_x + source_width, y1),
127
+ (intermediate_x, 0, intermediate_x + intermediate_width, height),
128
+ ),
129
+ )
130
+ )
131
+
132
+ if not candidates:
133
+ return PassPlan("direct", None, prepared.bbox, None)
134
+ return min(candidates, key=lambda item: (item[0], item[1].axis))[1]
135
+
136
+
137
+ def composite(generated: Image.Image, prepared: RegisteredSource) -> Image.Image:
138
+ generated = generated.convert("RGB")
139
+ if generated.size != prepared.canvas_size:
140
+ raise ValueError("Generated image size does not match the canvas")
141
+
142
+ width, height = prepared.placed_source.size
143
+ yy, xx = np.mgrid[:height, :width]
144
+ edge_distance = np.minimum.reduce((xx, yy, width - 1 - xx, height - 1 - yy))
145
+ alpha = np.clip(edge_distance / max(1, prepared.seam_px), 0.0, 1.0)
146
+ alpha_image = Image.fromarray((alpha * 255).astype(np.uint8), mode="L")
147
+ result = generated.copy()
148
+ result.paste(prepared.placed_source, prepared.bbox[:2], alpha_image)
149
+ return result
150
+
pipeline.py ADDED
@@ -0,0 +1,1522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Ostris, LLC. All rights reserved.
2
+ #
3
+ # Portions of the Krea2Transformer2DModel implementation are adapted from
4
+ # huggingface/diffusers (Apache License, Version 2.0), Copyright 2026 Krea AI
5
+ # and The HuggingFace Team.
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ """Krea2OstrisEdit -- a self-contained Hugging Face community pipeline for Krea 2
19
+ with reference-image (edit) conditioning and Ostris AI-Toolkit LoRA loading.
20
+
21
+ Everything lives in this one file so it can be hosted as a hub community
22
+ pipeline (a model repo containing just this ``pipeline.py``):
23
+
24
+ ```python
25
+ import torch
26
+ from diffusers import DiffusionPipeline
27
+ from PIL import Image
28
+
29
+ pipe = DiffusionPipeline.from_pretrained(
30
+ "krea/Krea-2-Turbo",
31
+ custom_pipeline="ostris/Krea2OstrisEdit",
32
+ torch_dtype=torch.bfloat16,
33
+ )
34
+ pipe.to("cuda") # or pipe.enable_model_cpu_offload() on GPUs with < ~40 GB VRAM
35
+
36
+ # Load an AI-Toolkit (or already-diffusers-format) Krea 2 LoRA, e.g. the style
37
+ # reference LoRA (generates the prompt in the style of the reference images).
38
+ pipe.load_lora_weights(
39
+ "ostris/krea2_turbo_style_reference", weight_name="krea2_style_reference.safetensors"
40
+ )
41
+
42
+ image = pipe(
43
+ "a white yeti with horns reading a book",
44
+ image=Image.open("style_reference.png"), # one reference image or a list of them
45
+ num_inference_steps=8, # Turbo defaults; the base model wants 28 / 4.5
46
+ guidance_scale=0.0,
47
+ # kv_cache=True, # reference K/V computed once and reused every step; only for
48
+ # # LoRAs trained with AI-Toolkit's kv_cache model kwarg
49
+ ).images[0]
50
+ image.save("output.png")
51
+ ```
52
+
53
+ Reference images condition the model in two places, matching how the edit LoRAs
54
+ are trained with Ostris AI-Toolkit (and the ComfyUI-Krea2-Ostris-Edit nodes):
55
+
56
+ 1. through the Qwen3-VL text encoder: each image is embedded in the user message
57
+ ahead of the prompt via ``Picture N: <|vision_start|><|image_pad|><|vision_end|>``
58
+ placeholders, so the text embeddings "see" the references;
59
+ 2. as clean VAE latents appended after the noisy image tokens in the transformer
60
+ sequence. They keep the flow time ``t=0`` (they are never noised) and sit on
61
+ rotary-position frame axis ``i + 1`` -- the Kontext-style "index" placement.
62
+
63
+ Without ``image`` the pipeline is a plain Krea 2 text-to-image sampler.
64
+ """
65
+
66
+ import math
67
+ import os
68
+ import re
69
+ from dataclasses import dataclass
70
+ from typing import Any, Dict, List, Optional, Tuple, Union
71
+
72
+ import numpy as np
73
+ import PIL.Image
74
+ import torch
75
+ import torch.nn as nn
76
+ import torch.nn.functional as F
77
+
78
+ import diffusers
79
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
80
+ from diffusers.image_processor import VaeImageProcessor
81
+ from diffusers.loaders import PeftAdapterMixin
82
+ from diffusers.models import AutoencoderKLQwenImage
83
+ from diffusers.models.modeling_outputs import Transformer2DModelOutput
84
+ from diffusers.models.modeling_utils import ModelMixin
85
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline
86
+ from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
87
+ from diffusers.utils import USE_PEFT_BACKEND, BaseOutput, logging, scale_lora_layers, unscale_lora_layers
88
+ from diffusers.utils.torch_utils import randn_tensor
89
+
90
+ try:
91
+ from transformers import AutoTokenizer, Qwen3VLModel
92
+ except ImportError as e: # pragma: no cover
93
+ raise ImportError(
94
+ "Krea2OstrisEdit requires a transformers version that ships Qwen3-VL "
95
+ "(`transformers>=4.57`). Please upgrade transformers."
96
+ ) from e
97
+
98
+
99
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
100
+
101
+ # torch>=2.5 supports grouped-query attention natively in SDPA; older versions
102
+ # need the key/value heads repeated to the query head count.
103
+ _SDPA_HAS_GQA = tuple(int(re.sub(r"\D.*", "", v) or 0) for v in torch.__version__.split(".")[:2]) >= (2, 5)
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # Transformer (Krea 2 single-stream MMDiT)
108
+ #
109
+ # Module tree and state-dict keys match the `Krea2Transformer2DModel` checkpoint
110
+ # layout in the `transformer/` folder of the Krea 2 hub repos, so the sharded
111
+ # weights load directly. The forward pass additionally supports clean reference
112
+ # tokens appended after the image tokens (`ref_seq_len`), which are modulated at
113
+ # flow time t=0 while the text + noisy image tokens keep the real timestep.
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ class Krea2RMSNorm(nn.Module):
118
+ """RMSNorm with a zero-centered scale: the effective multiplier is ``1 + weight``,
119
+ matching the Krea 2 checkpoint format. Normalization runs in float32."""
120
+
121
+ def __init__(self, dim: int, eps: float = 1e-5) -> None:
122
+ super().__init__()
123
+ self.dim = dim
124
+ self.eps = eps
125
+ self.weight = nn.Parameter(torch.zeros(dim))
126
+
127
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
128
+ dtype = hidden_states.dtype
129
+ hidden_states = F.rms_norm(
130
+ hidden_states.float(), (self.dim,), weight=self.weight.float() + 1.0, eps=self.eps
131
+ )
132
+ return hidden_states.to(dtype)
133
+
134
+
135
+ def _apply_rotary_emb(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
136
+ """Rotate interleaved (even, odd) channel pairs. ``x`` is (B, H, S, D); ``cos``/``sin``
137
+ are (S, D) in the repeat-interleaved layout produced by ``Krea2RotaryPosEmbed``."""
138
+ x_f = x.float()
139
+ x_rot = torch.stack((-x_f[..., 1::2], x_f[..., 0::2]), dim=-1).flatten(-2)
140
+ return (x_f * cos + x_rot * sin).to(x.dtype)
141
+
142
+
143
+ class Krea2RotaryPosEmbed(nn.Module):
144
+ def __init__(self, theta: float, axes_dim: List[int]) -> None:
145
+ super().__init__()
146
+ self.theta = theta
147
+ self.axes_dim = axes_dim
148
+
149
+ def forward(self, ids: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
150
+ # ids: (seq_len, 3) rotary coordinates. Frequencies are computed in float64
151
+ # (float32 on backends without float64 support, e.g. MPS).
152
+ dtype = torch.float32 if ids.device.type == "mps" else torch.float64
153
+ angles = []
154
+ for i, dim in enumerate(self.axes_dim):
155
+ pos = ids[:, i].to(dtype)
156
+ freqs = 1.0 / (self.theta ** (torch.arange(0, dim, 2, dtype=dtype, device=ids.device) / dim))
157
+ angles.append(pos[:, None] * freqs[None, :])
158
+ angles = torch.cat(angles, dim=-1)
159
+ cos = angles.cos().repeat_interleave(2, dim=-1).float()
160
+ sin = angles.sin().repeat_interleave(2, dim=-1).float()
161
+ return cos, sin
162
+
163
+
164
+ class Krea2Attention(nn.Module):
165
+ """Self-attention with grouped-query projections, q/k RMSNorm, rotary embeddings
166
+ and a sigmoid output gate."""
167
+
168
+ def __init__(self, hidden_size: int, num_heads: int, num_kv_heads: Optional[int] = None, eps: float = 1e-5):
169
+ super().__init__()
170
+ self.num_heads = num_heads
171
+ self.num_kv_heads = num_kv_heads if num_kv_heads is not None else num_heads
172
+ self.head_dim = hidden_size // num_heads
173
+
174
+ self.to_q = nn.Linear(hidden_size, self.head_dim * self.num_heads, bias=False)
175
+ self.to_k = nn.Linear(hidden_size, self.head_dim * self.num_kv_heads, bias=False)
176
+ self.to_v = nn.Linear(hidden_size, self.head_dim * self.num_kv_heads, bias=False)
177
+ self.to_gate = nn.Linear(hidden_size, hidden_size, bias=False)
178
+ self.norm_q = Krea2RMSNorm(self.head_dim, eps=eps)
179
+ self.norm_k = Krea2RMSNorm(self.head_dim, eps=eps)
180
+ self.to_out = nn.ModuleList([nn.Linear(hidden_size, hidden_size, bias=False), nn.Dropout(0.0)])
181
+
182
+ def forward(
183
+ self,
184
+ hidden_states: torch.Tensor,
185
+ attention_mask: Optional[torch.Tensor] = None,
186
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
187
+ kv_capture: Optional[list] = None,
188
+ kv_cache: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
189
+ ) -> torch.Tensor:
190
+ query = self.to_q(hidden_states).unflatten(-1, (self.num_heads, self.head_dim)).transpose(1, 2)
191
+ key = self.to_k(hidden_states).unflatten(-1, (self.num_kv_heads, self.head_dim)).transpose(1, 2)
192
+ value = self.to_v(hidden_states).unflatten(-1, (self.num_kv_heads, self.head_dim)).transpose(1, 2)
193
+ gate = self.to_gate(hidden_states)
194
+
195
+ query = self.norm_q(query)
196
+ key = self.norm_k(key)
197
+
198
+ if image_rotary_emb is not None:
199
+ cos, sin = image_rotary_emb
200
+ query = _apply_rotary_emb(query, cos, sin)
201
+ key = _apply_rotary_emb(key, cos, sin)
202
+
203
+ if kv_capture is not None:
204
+ kv_capture.append((key, value))
205
+ if kv_cache is not None:
206
+ # Cached reference K/V, already rotary-embedded at their original positions.
207
+ key = torch.cat([key, kv_cache[0].to(key.dtype)], dim=2)
208
+ value = torch.cat([value, kv_cache[1].to(value.dtype)], dim=2)
209
+
210
+ is_gqa = self.num_heads != self.num_kv_heads
211
+ if is_gqa and not _SDPA_HAS_GQA:
212
+ key = key.repeat_interleave(self.num_heads // self.num_kv_heads, dim=1)
213
+ value = value.repeat_interleave(self.num_heads // self.num_kv_heads, dim=1)
214
+ sdpa_kwargs = {"enable_gqa": True} if (is_gqa and _SDPA_HAS_GQA) else {}
215
+ hidden_states = F.scaled_dot_product_attention(query, key, value, attn_mask=attention_mask, **sdpa_kwargs)
216
+
217
+ hidden_states = hidden_states.transpose(1, 2).flatten(2)
218
+ hidden_states = hidden_states * torch.sigmoid(gate)
219
+ return self.to_out[0](hidden_states)
220
+
221
+
222
+ class Krea2SwiGLU(nn.Module):
223
+ def __init__(self, dim: int, hidden_dim: int) -> None:
224
+ super().__init__()
225
+ self.gate = nn.Linear(dim, hidden_dim, bias=False)
226
+ self.up = nn.Linear(dim, hidden_dim, bias=False)
227
+ self.down = nn.Linear(hidden_dim, dim, bias=False)
228
+
229
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
230
+ return self.down(F.silu(self.gate(hidden_states)) * self.up(hidden_states))
231
+
232
+
233
+ class Krea2TextFusionBlock(nn.Module):
234
+ """Pre-norm transformer block (no rotary embeddings, no time modulation) used by
235
+ the text fusion stage."""
236
+
237
+ def __init__(self, dim: int, num_heads: int, num_kv_heads: int, intermediate_size: int, eps: float) -> None:
238
+ super().__init__()
239
+ self.norm1 = Krea2RMSNorm(dim, eps=eps)
240
+ self.norm2 = Krea2RMSNorm(dim, eps=eps)
241
+ self.attn = Krea2Attention(dim, num_heads, num_kv_heads, eps=eps)
242
+ self.ff = Krea2SwiGLU(dim, intermediate_size)
243
+
244
+ def forward(self, hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
245
+ hidden_states = hidden_states + self.attn(self.norm1(hidden_states), attention_mask=attention_mask)
246
+ hidden_states = hidden_states + self.ff(self.norm2(hidden_states))
247
+ return hidden_states
248
+
249
+
250
+ class Krea2TextFusion(nn.Module):
251
+ """Fuses the stack of tapped text-encoder hidden states into one text sequence:
252
+ ``layerwise_blocks`` attend across the layer axis per token, a linear ``projector``
253
+ collapses that axis, and ``refiner_blocks`` attend across the token sequence."""
254
+
255
+ def __init__(
256
+ self,
257
+ num_text_layers: int,
258
+ dim: int,
259
+ num_heads: int,
260
+ num_kv_heads: int,
261
+ intermediate_size: int,
262
+ num_layerwise_blocks: int,
263
+ num_refiner_blocks: int,
264
+ eps: float,
265
+ ) -> None:
266
+ super().__init__()
267
+ self.layerwise_blocks = nn.ModuleList(
268
+ [
269
+ Krea2TextFusionBlock(dim, num_heads, num_kv_heads, intermediate_size, eps)
270
+ for _ in range(num_layerwise_blocks)
271
+ ]
272
+ )
273
+ self.projector = nn.Linear(num_text_layers, 1, bias=False)
274
+ self.refiner_blocks = nn.ModuleList(
275
+ [
276
+ Krea2TextFusionBlock(dim, num_heads, num_kv_heads, intermediate_size, eps)
277
+ for _ in range(num_refiner_blocks)
278
+ ]
279
+ )
280
+
281
+ def forward(self, encoder_hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor] = None):
282
+ batch_size, seq_len, num_text_layers, dim = encoder_hidden_states.shape
283
+
284
+ hidden_states = encoder_hidden_states.reshape(batch_size * seq_len, num_text_layers, dim)
285
+ for block in self.layerwise_blocks:
286
+ hidden_states = block(hidden_states.contiguous())
287
+
288
+ hidden_states = hidden_states.reshape(batch_size, seq_len, num_text_layers, dim).permute(0, 1, 3, 2)
289
+ hidden_states = self.projector(hidden_states).squeeze(-1)
290
+
291
+ for block in self.refiner_blocks:
292
+ hidden_states = block(hidden_states, attention_mask=attention_mask)
293
+
294
+ return hidden_states
295
+
296
+
297
+ class Krea2TransformerBlock(nn.Module):
298
+ def __init__(
299
+ self, hidden_size: int, intermediate_size: int, num_heads: int, num_kv_heads: int, norm_eps: float
300
+ ) -> None:
301
+ super().__init__()
302
+ self.scale_shift_table = nn.Parameter(torch.zeros(6, hidden_size))
303
+ self.norm1 = Krea2RMSNorm(hidden_size, eps=norm_eps)
304
+ self.norm2 = Krea2RMSNorm(hidden_size, eps=norm_eps)
305
+ self.attn = Krea2Attention(hidden_size, num_heads, num_kv_heads, eps=norm_eps)
306
+ self.ff = Krea2SwiGLU(hidden_size, intermediate_size)
307
+
308
+ def forward(
309
+ self,
310
+ hidden_states: torch.Tensor,
311
+ temb: Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor, int]],
312
+ image_rotary_emb: Tuple[torch.Tensor, torch.Tensor],
313
+ attention_mask: Optional[torch.Tensor] = None,
314
+ kv_capture: Optional[list] = None,
315
+ kv_cache: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
316
+ ) -> torch.Tensor:
317
+ # ``temb`` is the (B, 1, 6 * hidden_size) modulation input, or a tuple
318
+ # ``(temb, ref_temb, split)`` for reference-image conditioning: tokens
319
+ # ``[:split]`` (text + noisy image) are modulated with the real timestep
320
+ # while tokens ``[split:]`` (clean reference tokens) use the t=0 embedding.
321
+ if isinstance(temb, tuple):
322
+ temb, ref_temb, split = temb
323
+ m = (temb.unflatten(-1, (6, -1)) + self.scale_shift_table).unbind(-2)
324
+ r = (ref_temb.unflatten(-1, (6, -1)) + self.scale_shift_table).unbind(-2)
325
+
326
+ def modulate(h, scale_idx, shift_idx):
327
+ return torch.cat(
328
+ (
329
+ (1.0 + m[scale_idx]) * h[:, :split] + m[shift_idx],
330
+ (1.0 + r[scale_idx]) * h[:, split:] + r[shift_idx],
331
+ ),
332
+ dim=1,
333
+ )
334
+
335
+ def gate(h, gate_idx):
336
+ return torch.cat((m[gate_idx] * h[:, :split], r[gate_idx] * h[:, split:]), dim=1)
337
+
338
+ attn_out = self.attn(
339
+ modulate(self.norm1(hidden_states), 0, 1),
340
+ attention_mask=attention_mask,
341
+ image_rotary_emb=image_rotary_emb,
342
+ kv_capture=kv_capture,
343
+ kv_cache=kv_cache,
344
+ )
345
+ hidden_states = hidden_states + gate(attn_out, 2)
346
+ ff_out = self.ff(modulate(self.norm2(hidden_states), 3, 4))
347
+ hidden_states = hidden_states + gate(ff_out, 5)
348
+ return hidden_states
349
+
350
+ modulation = temb.unflatten(-1, (6, -1)) + self.scale_shift_table
351
+ prescale, preshift, pregate, postscale, postshift, postgate = modulation.unbind(-2)
352
+
353
+ attn_out = self.attn(
354
+ (1.0 + prescale) * self.norm1(hidden_states) + preshift,
355
+ attention_mask=attention_mask,
356
+ image_rotary_emb=image_rotary_emb,
357
+ kv_capture=kv_capture,
358
+ kv_cache=kv_cache,
359
+ )
360
+ hidden_states = hidden_states + pregate * attn_out
361
+ ff_out = self.ff((1.0 + postscale) * self.norm2(hidden_states) + postshift)
362
+ hidden_states = hidden_states + postgate * ff_out
363
+ return hidden_states
364
+
365
+
366
+ class Krea2TimestepEmbedding(nn.Module):
367
+ """Sinusoidal flow-time embedding (cos-first, input scaled by 1000) followed by a
368
+ two-layer MLP. Keeps the sequence dimension at size 1 so per-block modulations
369
+ broadcast over tokens."""
370
+
371
+ def __init__(self, embed_dim: int, hidden_size: int) -> None:
372
+ super().__init__()
373
+ self.embed_dim = embed_dim
374
+ self.linear_1 = nn.Linear(embed_dim, hidden_size, bias=True)
375
+ self.linear_2 = nn.Linear(hidden_size, hidden_size, bias=True)
376
+
377
+ def forward(self, timestep: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
378
+ half = self.embed_dim // 2
379
+ freqs = torch.exp(-math.log(1e4) * torch.arange(half, dtype=torch.float32, device=timestep.device) / half)
380
+ args = (timestep.float() * 1e3)[:, None, None] * freqs
381
+ emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1).to(dtype)
382
+ return self.linear_2(F.gelu(self.linear_1(emb), approximate="tanh"))
383
+
384
+
385
+ class Krea2TextProjection(nn.Module):
386
+ """Projects the fused text features into the transformer width."""
387
+
388
+ def __init__(self, text_dim: int, hidden_size: int, eps: float) -> None:
389
+ super().__init__()
390
+ self.norm = Krea2RMSNorm(text_dim, eps=eps)
391
+ self.linear_1 = nn.Linear(text_dim, hidden_size, bias=True)
392
+ self.linear_2 = nn.Linear(hidden_size, hidden_size, bias=True)
393
+
394
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
395
+ hidden_states = self.linear_1(self.norm(hidden_states))
396
+ return self.linear_2(F.gelu(hidden_states, approximate="tanh"))
397
+
398
+
399
+ class Krea2FinalLayer(nn.Module):
400
+ """Final adaptive RMSNorm and output projection."""
401
+
402
+ def __init__(self, hidden_size: int, out_channels: int, eps: float) -> None:
403
+ super().__init__()
404
+ self.scale_shift_table = nn.Parameter(torch.zeros(2, hidden_size))
405
+ self.norm = Krea2RMSNorm(hidden_size, eps=eps)
406
+ self.linear = nn.Linear(hidden_size, out_channels, bias=True)
407
+
408
+ def forward(self, hidden_states: torch.Tensor, temb: torch.Tensor) -> torch.Tensor:
409
+ modulation = temb + self.scale_shift_table
410
+ scale, shift = modulation.chunk(2, dim=1)
411
+ hidden_states = (1.0 + scale) * self.norm(hidden_states) + shift
412
+ return self.linear(hidden_states)
413
+
414
+
415
+ class Krea2Transformer2DModel(ModelMixin, ConfigMixin, PeftAdapterMixin):
416
+ r"""
417
+ The Krea 2 single-stream MMDiT flow-matching backbone, extended with support for
418
+ clean reference-image tokens ("edit" conditioning).
419
+
420
+ Text conditioning enters as a stack of hidden states tapped from several layers of
421
+ the Qwen3-VL text encoder. A small text-fusion transformer collapses the layer axis
422
+ and refines the token sequence; the result is concatenated with the patchified
423
+ image latents (and, optionally, packed reference latents) into a single
424
+ ``[text, image, refs]`` sequence processed by the transformer blocks.
425
+
426
+ When ``ref_seq_len > 0``, the last ``ref_seq_len`` tokens of ``hidden_states`` are
427
+ clean reference tokens: they are modulated with the t=0 timestep embedding
428
+ (Kontext-style "index_timestep_zero") and excluded from the returned velocity.
429
+ """
430
+
431
+ _supports_gradient_checkpointing = True
432
+ _no_split_modules = ["Krea2TransformerBlock", "Krea2TextFusionBlock", "Krea2FinalLayer"]
433
+ _keep_in_fp32_modules = ["norm", "norm1", "norm2", "norm_q", "norm_k"]
434
+ _skip_layerwise_casting_patterns = ["time_embed", "norm"]
435
+
436
+ @register_to_config
437
+ def __init__(
438
+ self,
439
+ in_channels: int = 64,
440
+ num_layers: int = 28,
441
+ attention_head_dim: int = 128,
442
+ num_attention_heads: int = 48,
443
+ num_key_value_heads: int = 12,
444
+ intermediate_size: int = 16384,
445
+ timestep_embed_dim: int = 256,
446
+ text_hidden_dim: int = 2560,
447
+ num_text_layers: int = 12,
448
+ text_num_attention_heads: int = 20,
449
+ text_num_key_value_heads: int = 20,
450
+ text_intermediate_size: int = 6912,
451
+ num_layerwise_text_blocks: int = 2,
452
+ num_refiner_text_blocks: int = 2,
453
+ axes_dims_rope: Tuple[int, int, int] = (32, 48, 48),
454
+ rope_theta: float = 1000.0,
455
+ norm_eps: float = 1e-5,
456
+ ) -> None:
457
+ super().__init__()
458
+
459
+ hidden_size = attention_head_dim * num_attention_heads
460
+ if sum(axes_dims_rope) != attention_head_dim:
461
+ raise ValueError(
462
+ f"sum(axes_dims_rope)={sum(axes_dims_rope)} must equal attention_head_dim={attention_head_dim}"
463
+ )
464
+
465
+ self.in_channels = in_channels
466
+ self.out_channels = in_channels
467
+ self.hidden_size = hidden_size
468
+ self.gradient_checkpointing = False
469
+
470
+ self.img_in = nn.Linear(in_channels, hidden_size, bias=True)
471
+ self.time_embed = Krea2TimestepEmbedding(timestep_embed_dim, hidden_size)
472
+ self.time_mod_proj = nn.Linear(hidden_size, 6 * hidden_size, bias=True)
473
+ self.text_fusion = Krea2TextFusion(
474
+ num_text_layers=num_text_layers,
475
+ dim=text_hidden_dim,
476
+ num_heads=text_num_attention_heads,
477
+ num_kv_heads=text_num_key_value_heads,
478
+ intermediate_size=text_intermediate_size,
479
+ num_layerwise_blocks=num_layerwise_text_blocks,
480
+ num_refiner_blocks=num_refiner_text_blocks,
481
+ eps=norm_eps,
482
+ )
483
+ self.txt_in = Krea2TextProjection(text_hidden_dim, hidden_size, eps=norm_eps)
484
+ self.rotary_emb = Krea2RotaryPosEmbed(theta=rope_theta, axes_dim=list(axes_dims_rope))
485
+
486
+ self.transformer_blocks = nn.ModuleList(
487
+ [
488
+ Krea2TransformerBlock(
489
+ hidden_size=hidden_size,
490
+ intermediate_size=intermediate_size,
491
+ num_heads=num_attention_heads,
492
+ num_kv_heads=num_key_value_heads,
493
+ norm_eps=norm_eps,
494
+ )
495
+ for _ in range(num_layers)
496
+ ]
497
+ )
498
+
499
+ self.final_layer = Krea2FinalLayer(hidden_size, out_channels=in_channels, eps=norm_eps)
500
+
501
+ def precompute_ref_kv(
502
+ self,
503
+ hidden_states: torch.Tensor,
504
+ position_ids: torch.Tensor,
505
+ attention_kwargs: Optional[Dict[str, Any]] = None,
506
+ ) -> List[Tuple[torch.Tensor, torch.Tensor]]:
507
+ r"""
508
+ Run only the packed clean reference tokens through the transformer blocks at t=0
509
+ and return each block's rotary-embedded key/value pair.
510
+
511
+ Only valid for adapters trained with AI-Toolkit's ``kv_cache`` model kwarg,
512
+ where reference tokens attend solely to each other: their per-block K/V are
513
+ then independent of the timestep and of the rest of the sequence, so this
514
+ single pass serves every denoising step. Pass the result to
515
+ ``forward(..., ref_kv_cache=...)`` with the reference tokens dropped from
516
+ ``hidden_states`` and ``position_ids``.
517
+
518
+ Args:
519
+ hidden_states (`torch.Tensor` of shape `(batch_size, ref_seq_len, in_channels)`):
520
+ Packed reference latents (see the pipeline's `_pack_reference_latents`).
521
+ position_ids (`torch.Tensor` of shape `(ref_seq_len, 3)`):
522
+ The reference tokens' rotary coordinates.
523
+ attention_kwargs (`dict`, *optional*):
524
+ When it contains a `scale` entry, sets the LoRA scale, matching `forward`.
525
+
526
+ Returns:
527
+ A list with one `(key, value)` tuple per transformer block, each of shape
528
+ `(batch_size, num_key_value_heads, ref_seq_len, attention_head_dim)`.
529
+ """
530
+ lora_scale = 1.0
531
+ if attention_kwargs is not None:
532
+ lora_scale = attention_kwargs.get("scale", 1.0)
533
+ if USE_PEFT_BACKEND and lora_scale != 1.0:
534
+ scale_lora_layers(self, lora_scale)
535
+
536
+ # Clean reference tokens are always conditioned at flow time t=0.
537
+ timestep = torch.zeros(hidden_states.shape[0], device=hidden_states.device)
538
+ temb = self.time_embed(timestep, dtype=hidden_states.dtype)
539
+ temb_mod = self.time_mod_proj(F.gelu(temb, approximate="tanh"))
540
+
541
+ hidden_states = self.img_in(hidden_states)
542
+ image_rotary_emb = self.rotary_emb(position_ids)
543
+
544
+ ref_kv = []
545
+ for block in self.transformer_blocks:
546
+ captured = []
547
+ hidden_states = block(hidden_states, temb_mod, image_rotary_emb, kv_capture=captured)
548
+ ref_kv.append(captured[0])
549
+
550
+ if USE_PEFT_BACKEND and lora_scale != 1.0:
551
+ unscale_lora_layers(self, lora_scale)
552
+ return ref_kv
553
+
554
+ def forward(
555
+ self,
556
+ hidden_states: torch.Tensor,
557
+ encoder_hidden_states: torch.Tensor,
558
+ timestep: torch.Tensor,
559
+ position_ids: torch.Tensor,
560
+ encoder_attention_mask: Optional[torch.Tensor] = None,
561
+ ref_seq_len: int = 0,
562
+ ref_kv_cache: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
563
+ attention_kwargs: Optional[Dict[str, Any]] = None,
564
+ return_dict: bool = True,
565
+ ) -> Union[Transformer2DModelOutput, Tuple[torch.Tensor]]:
566
+ r"""
567
+ Predict the flow-matching velocity for the (noisy) image tokens.
568
+
569
+ Args:
570
+ hidden_states (`torch.Tensor` of shape `(batch_size, image_seq_len + ref_seq_len, in_channels)`):
571
+ Packed (patchified) noisy image latents, with any packed clean reference
572
+ latents appended at the end.
573
+ encoder_hidden_states (`torch.Tensor` of shape `(batch_size, text_seq_len, num_text_layers, text_hidden_dim)`):
574
+ Stack of tapped text-encoder hidden states per token.
575
+ timestep (`torch.Tensor` of shape `(batch_size,)`):
576
+ Flow-matching time in `[0, 1]` (1 is pure noise, 0 is clean data).
577
+ position_ids (`torch.Tensor` of shape `(text_seq_len + image_seq_len + ref_seq_len, 3)`):
578
+ `(t, h, w)` rotary coordinates for the combined sequence. Text rows are
579
+ all-zero; image rows hold the latent-grid coordinates; the i-th
580
+ reference image sits on frame axis `i + 1` with its own grid.
581
+ encoder_attention_mask (`torch.Tensor` of shape `(batch_size, text_seq_len)`, *optional*):
582
+ Boolean mask marking valid text tokens.
583
+ ref_seq_len (`int`, defaults to 0):
584
+ Number of trailing reference tokens in `hidden_states`. They receive the
585
+ t=0 modulation and are excluded from the output.
586
+ ref_kv_cache (`list[tuple[torch.Tensor, torch.Tensor]]`, *optional*):
587
+ Per-block reference K/V from [`~Krea2Transformer2DModel.precompute_ref_kv`].
588
+ When given, `hidden_states` / `position_ids` must not contain the
589
+ reference tokens (`ref_seq_len == 0`); each block's attention appends the
590
+ cached K/V as extra keys instead. Only valid for adapters trained with
591
+ AI-Toolkit's ``kv_cache`` model kwarg (isolated reference attention).
592
+ attention_kwargs (`dict`, *optional*):
593
+ When it contains a `scale` entry, sets the LoRA scale applied to this
594
+ transformer's adapters for the duration of the forward pass.
595
+
596
+ Returns:
597
+ The velocity tensor of shape `(batch_size, image_seq_len, in_channels)`.
598
+ """
599
+ if position_ids.ndim != 2 or position_ids.shape[-1] != 3:
600
+ raise ValueError(f"`position_ids` must have shape (sequence_length, 3), got {tuple(position_ids.shape)}.")
601
+ if ref_kv_cache is not None and ref_seq_len > 0:
602
+ raise ValueError(
603
+ "`ref_kv_cache` replaces the reference tokens; do not also append them to "
604
+ "`hidden_states` (`ref_seq_len` must be 0)."
605
+ )
606
+
607
+ lora_scale = 1.0
608
+ if attention_kwargs is not None:
609
+ attention_kwargs = attention_kwargs.copy()
610
+ lora_scale = attention_kwargs.pop("scale", 1.0)
611
+ if USE_PEFT_BACKEND and lora_scale != 1.0:
612
+ scale_lora_layers(self, lora_scale)
613
+
614
+ batch_size, image_seq_len, _ = hidden_states.shape # includes ref tokens
615
+ text_seq_len = encoder_hidden_states.shape[1]
616
+
617
+ temb = self.time_embed(timestep, dtype=hidden_states.dtype)
618
+ temb_mod = self.time_mod_proj(F.gelu(temb, approximate="tanh"))
619
+
620
+ block_temb = temb_mod
621
+ if ref_seq_len > 0:
622
+ # Clean reference tokens are conditioned at t=0; everything else keeps t.
623
+ temb_zero = self.time_embed(torch.zeros_like(timestep), dtype=hidden_states.dtype)
624
+ ref_temb_mod = self.time_mod_proj(F.gelu(temb_zero, approximate="tanh"))
625
+ block_temb = (temb_mod, ref_temb_mod, text_seq_len + image_seq_len - ref_seq_len)
626
+
627
+ # An all-True mask (no padded text tokens, e.g. any batch-of-1 call) is
628
+ # equivalent to no mask; passing None keeps SDPA on its fast, low-memory
629
+ # (flash) path instead of a mask-materializing fallback.
630
+ if encoder_attention_mask is not None and bool(encoder_attention_mask.all()):
631
+ encoder_attention_mask = None
632
+
633
+ text_attention_mask = None
634
+ attention_mask = None
635
+ if encoder_attention_mask is not None:
636
+ # Key-padding masks of shape (B, 1, 1, L): padded text tokens are excluded
637
+ # as attention keys everywhere; their own (garbage) lanes are never read
638
+ # back and are dropped at the output slice.
639
+ text_attention_mask = encoder_attention_mask[:, None, None, :]
640
+ image_mask = encoder_attention_mask.new_ones((batch_size, image_seq_len))
641
+ attention_mask = torch.cat([encoder_attention_mask, image_mask], dim=1)[:, None, None, :]
642
+ if ref_kv_cache is not None:
643
+ # Cached reference K/V are appended as extra (always-valid) keys.
644
+ ref_mask = attention_mask.new_ones((batch_size, 1, 1, ref_kv_cache[0][0].shape[2]))
645
+ attention_mask = torch.cat([attention_mask, ref_mask], dim=-1)
646
+
647
+ encoder_hidden_states = self.text_fusion(encoder_hidden_states, attention_mask=text_attention_mask)
648
+ encoder_hidden_states = self.txt_in(encoder_hidden_states)
649
+
650
+ hidden_states = self.img_in(hidden_states)
651
+ hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1)
652
+
653
+ image_rotary_emb = self.rotary_emb(position_ids)
654
+
655
+ for i, block in enumerate(self.transformer_blocks):
656
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
657
+ ckpt_func = getattr(self, "_gradient_checkpointing_func", None)
658
+ if ckpt_func is None:
659
+ hidden_states = torch.utils.checkpoint.checkpoint(
660
+ block, hidden_states, block_temb, image_rotary_emb, attention_mask, use_reentrant=False
661
+ )
662
+ else:
663
+ hidden_states = ckpt_func(block, hidden_states, block_temb, image_rotary_emb, attention_mask)
664
+ else:
665
+ hidden_states = block(
666
+ hidden_states,
667
+ block_temb,
668
+ image_rotary_emb,
669
+ attention_mask,
670
+ kv_cache=ref_kv_cache[i] if ref_kv_cache is not None else None,
671
+ )
672
+
673
+ hidden_states = hidden_states[:, text_seq_len : text_seq_len + image_seq_len - ref_seq_len]
674
+ output = self.final_layer(hidden_states, temb)
675
+
676
+ if USE_PEFT_BACKEND and lora_scale != 1.0:
677
+ unscale_lora_layers(self, lora_scale)
678
+
679
+ if not return_dict:
680
+ return (output,)
681
+ return Transformer2DModelOutput(sample=output)
682
+
683
+
684
+ # The Krea 2 hub repos' `model_index.json` points the `transformer` component at
685
+ # `["diffusers", "Krea2Transformer2DModel"]`. Registering the vendored class into the
686
+ # diffusers namespace lets `DiffusionPipeline.from_pretrained` resolve it on diffusers
687
+ # releases that don't ship Krea 2 yet, and guarantees the loaded transformer supports
688
+ # the reference-image forward pass this pipeline needs (the class is a numerically
689
+ # identical superset of the upstream one for text-to-image).
690
+ diffusers.Krea2Transformer2DModel = Krea2Transformer2DModel
691
+
692
+
693
+ # ---------------------------------------------------------------------------
694
+ # LoRA key conversion (Ostris AI-Toolkit / reference-trainer -> diffusers/PEFT)
695
+ # ---------------------------------------------------------------------------
696
+
697
+
698
+ def _convert_non_diffusers_krea2_lora_to_diffusers(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
699
+ """Map original `krea-ai/krea-2` module names onto `Krea2Transformer2DModel`.
700
+ Handles the `diffusion_model.` prefix (AI-Toolkit saves / ComfyUI) and the
701
+ `base_model.model.` prefix, as well as bare module names."""
702
+ state_dict = {
703
+ (k[len("base_model.model.") :] if k.startswith("base_model.model.") else k): v for k, v in state_dict.items()
704
+ }
705
+ state_dict = {
706
+ (k[len("diffusion_model.") :] if k.startswith("diffusion_model.") else k): v for k, v in state_dict.items()
707
+ }
708
+
709
+ attn_map = {"wq": "to_q", "wk": "to_k", "wv": "to_v", "wo": "to_out.0", "gate": "to_gate"}
710
+ ff_map = {"gate": "ff.gate", "up": "ff.up", "down": "ff.down"}
711
+ # The original model stores these standalone modules under abbreviated
712
+ # `nn.Sequential`-style names.
713
+ standalone_map = {
714
+ "first": "img_in",
715
+ "last.linear": "final_layer.linear",
716
+ "tmlp.0": "time_embed.linear_1",
717
+ "tmlp.2": "time_embed.linear_2",
718
+ "tproj.1": "time_mod_proj",
719
+ "txtmlp.1": "txt_in.linear_1",
720
+ "txtmlp.3": "txt_in.linear_2",
721
+ "txtfusion.projector": "text_fusion.projector",
722
+ }
723
+
724
+ def convert_module(module):
725
+ m = re.match(r"blocks\.(\d+)\.(attn|mlp)\.(\w+)$", module)
726
+ if m:
727
+ idx, kind, sub = m.groups()
728
+ if kind == "attn" and sub in attn_map:
729
+ return f"transformer_blocks.{idx}.attn.{attn_map[sub]}"
730
+ if kind == "mlp" and sub in ff_map:
731
+ return f"transformer_blocks.{idx}.{ff_map[sub]}"
732
+ return None
733
+ m = re.match(r"txtfusion\.(layerwise_blocks|refiner_blocks)\.(\d+)\.(attn|mlp)\.(\w+)$", module)
734
+ if m:
735
+ block, idx, kind, sub = m.groups()
736
+ if kind == "attn" and sub in attn_map:
737
+ return f"text_fusion.{block}.{idx}.attn.{attn_map[sub]}"
738
+ if kind == "mlp" and sub in ff_map:
739
+ return f"text_fusion.{block}.{idx}.{ff_map[sub]}"
740
+ return None
741
+ return standalone_map.get(module)
742
+
743
+ converted_state_dict = {}
744
+ for key in list(state_dict):
745
+ match = re.search(r"\.(?:lora_[AB])\.weight$", key)
746
+ if match is None:
747
+ continue
748
+ diffusers_module = convert_module(key[: match.start()])
749
+ if diffusers_module is None:
750
+ continue
751
+ converted_state_dict[f"transformer.{diffusers_module}{key[match.start() :]}"] = state_dict.pop(key)
752
+
753
+ if len(state_dict) > 0:
754
+ raise ValueError(f"Could not convert LoRA keys: {sorted(state_dict.keys())}")
755
+
756
+ return converted_state_dict
757
+
758
+
759
+ def _normalize_lora_state_dict(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
760
+ """Normalize a Krea 2 LoRA state dict to PEFT `lora_A`/`lora_B` naming and fold any
761
+ `.alpha` tensors into `lora_B` so the effective scale is preserved."""
762
+ state_dict = {
763
+ k.replace(".lora_down.weight", ".lora_A.weight").replace(".lora_up.weight", ".lora_B.weight"): v
764
+ for k, v in state_dict.items()
765
+ }
766
+ # PEFT assumes lora_alpha == rank (scale 1.0) when no alpha is given; fold any
767
+ # explicit alpha into lora_B instead of plumbing network_alphas through.
768
+ for alpha_key in [k for k in state_dict if k.endswith(".alpha")]:
769
+ base = alpha_key[: -len(".alpha")]
770
+ a_key, b_key = base + ".lora_A.weight", base + ".lora_B.weight"
771
+ alpha = float(state_dict.pop(alpha_key))
772
+ if a_key in state_dict and b_key in state_dict:
773
+ rank = state_dict[a_key].shape[0]
774
+ if alpha != rank:
775
+ state_dict[b_key] = state_dict[b_key] * (alpha / rank)
776
+ return state_dict
777
+
778
+
779
+ # ---------------------------------------------------------------------------
780
+ # Pipeline
781
+ # ---------------------------------------------------------------------------
782
+
783
+
784
+ @dataclass
785
+ class Krea2PipelineOutput(BaseOutput):
786
+ """Output class for the Krea 2 pipeline.
787
+
788
+ Args:
789
+ images (`list[PIL.Image.Image]` or `np.ndarray`):
790
+ List of `num_batches * num_images_per_prompt` denoised PIL images or a
791
+ numpy array of shape `(batch_size, height, width, num_channels)`.
792
+ """
793
+
794
+ images: Union[List[PIL.Image.Image], np.ndarray]
795
+
796
+
797
+ def calculate_shift(
798
+ image_seq_len,
799
+ base_seq_len: int = 256,
800
+ max_seq_len: int = 6400,
801
+ base_shift: float = 0.5,
802
+ max_shift: float = 1.15,
803
+ ):
804
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
805
+ b = base_shift - m * base_seq_len
806
+ mu = image_seq_len * m + b
807
+ return mu
808
+
809
+
810
+ class Krea2OstrisEditPipeline(DiffusionPipeline):
811
+ r"""
812
+ Krea 2 text-to-image / reference-image-edit pipeline with Ostris AI-Toolkit LoRA
813
+ loading. See the module docstring for usage.
814
+
815
+ Args:
816
+ scheduler ([`FlowMatchEulerDiscreteScheduler`]):
817
+ Euler flow-matching scheduler configured with the Krea 2 resolution-aware
818
+ exponential time shift.
819
+ vae ([`AutoencoderKLQwenImage`]):
820
+ The Qwen-Image VAE (f8, 16 latent channels).
821
+ text_encoder ([`~transformers.Qwen3VLModel`]):
822
+ Qwen3-VL, including its vision tower (used to embed reference images into
823
+ the prompt conditioning).
824
+ tokenizer ([`~transformers.AutoTokenizer`]):
825
+ The tokenizer paired with the text encoder.
826
+ transformer ([`Krea2Transformer2DModel`]):
827
+ The Krea 2 single-stream MMDiT.
828
+ text_encoder_select_layers (`tuple[int, ...]`, *optional*):
829
+ Indices into the text encoder's `hidden_states` tuple whose states are
830
+ stacked per token as the transformer's text conditioning.
831
+ is_distilled (`bool`, *optional*, defaults to `False`):
832
+ Whether the transformer is the few-step distilled (Turbo) checkpoint. When
833
+ `True`, a fixed timestep shift `mu=1.15` is used and the call defaults
834
+ change to `num_inference_steps=8, guidance_scale=0.0`.
835
+ patch_size (`int`, *optional*, defaults to 2):
836
+ Side length of the square patches the latents are packed into.
837
+ """
838
+
839
+ model_cpu_offload_seq = "text_encoder->transformer->vae"
840
+
841
+ # Default hub repo used to lazily build the Qwen3-VL processor that turns
842
+ # reference images into vision tokens (the Krea 2 repos ship only a tokenizer).
843
+ vl_processor_id = "Qwen/Qwen3-VL-4B-Instruct"
844
+
845
+ def __init__(
846
+ self,
847
+ scheduler: FlowMatchEulerDiscreteScheduler,
848
+ vae: AutoencoderKLQwenImage,
849
+ text_encoder: Qwen3VLModel,
850
+ tokenizer: AutoTokenizer,
851
+ transformer: Krea2Transformer2DModel,
852
+ text_encoder_select_layers: Optional[Union[Tuple[int, ...], List[int]]] = None,
853
+ is_distilled: bool = False,
854
+ patch_size: int = 2,
855
+ ):
856
+ super().__init__()
857
+
858
+ self.register_modules(
859
+ scheduler=scheduler,
860
+ vae=vae,
861
+ text_encoder=text_encoder,
862
+ tokenizer=tokenizer,
863
+ transformer=transformer,
864
+ )
865
+ if text_encoder_select_layers is None:
866
+ text_encoder_select_layers = (2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35)
867
+ self.register_to_config(text_encoder_select_layers=tuple(text_encoder_select_layers))
868
+ self.text_encoder_select_layers = tuple(text_encoder_select_layers)
869
+ self.register_to_config(is_distilled=is_distilled)
870
+ self.vae_scale_factor = 2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8
871
+ self.register_to_config(patch_size=patch_size)
872
+ self.patch_size = patch_size
873
+ self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * self.patch_size)
874
+
875
+ # Fixed instruction template wrapped around every prompt. The system prefix is
876
+ # fed through the encoder as context but its hidden states are sliced off.
877
+ self.prompt_template_encode_prefix = (
878
+ "<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, text, "
879
+ "spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n"
880
+ )
881
+ self.prompt_template_encode_suffix = "<|im_end|>\n<|im_start|>assistant\n"
882
+ self.prompt_template_encode_start_idx = 34
883
+
884
+ self._vl_processor = None
885
+
886
+ # ------------------------------------------------------------------
887
+ # Prompt encoding (Qwen3-VL; reference images embedded via vision tokens)
888
+ # ------------------------------------------------------------------
889
+ @property
890
+ def vl_processor(self):
891
+ """Qwen3-VL AutoProcessor, loaded lazily (only needed when reference images are
892
+ encoded into the prompt)."""
893
+ if self._vl_processor is None:
894
+ from transformers import AutoProcessor
895
+
896
+ self._vl_processor = AutoProcessor.from_pretrained(self.vl_processor_id)
897
+ return self._vl_processor
898
+
899
+ @staticmethod
900
+ def _to_chw_tensor(image) -> torch.Tensor:
901
+ """Convert a PIL image / numpy array / CHW tensor to a float CHW tensor in [0, 1]."""
902
+ if isinstance(image, torch.Tensor):
903
+ t = image.squeeze(0) if image.ndim == 4 else image
904
+ t = t.float()
905
+ if t.min() < 0: # assume [-1, 1]
906
+ t = (t + 1.0) / 2.0
907
+ return t.clamp(0, 1)
908
+ if isinstance(image, np.ndarray):
909
+ image = PIL.Image.fromarray(image)
910
+ image = image.convert("RGB")
911
+ arr = np.asarray(image).astype(np.float32) / 255.0
912
+ return torch.from_numpy(arr).permute(2, 0, 1)
913
+
914
+ def _prep_vl_images(self, images: List[torch.Tensor], max_pixels: int) -> List[torch.Tensor]:
915
+ """Resize reference images for the Qwen3-VL pass: aspect-preserving downscale
916
+ (never upscaled) to fit ``max_pixels`` total area. The MLLM only needs a coarse
917
+ view of the references; high-res detail flows through the VAE ref latents."""
918
+ prepped = []
919
+ for img in images:
920
+ h, w = img.shape[1], img.shape[2]
921
+ scale = min(1.0, math.sqrt(max_pixels / (h * w)))
922
+ nh, nw = max(round(h * scale), 28), max(round(w * scale), 28)
923
+ if (nh, nw) != (h, w):
924
+ img = (
925
+ F.interpolate(img.unsqueeze(0).float(), size=(nh, nw), mode="bicubic", antialias=True)
926
+ .squeeze(0)
927
+ .clamp(0, 1)
928
+ )
929
+ prepped.append(img.float())
930
+ return prepped
931
+
932
+ def _encode_single_prompt(
933
+ self,
934
+ prompt: str,
935
+ images: Optional[List[torch.Tensor]] = None,
936
+ max_sequence_length: int = 512,
937
+ device: Optional[torch.device] = None,
938
+ ) -> torch.Tensor:
939
+ """Encode one prompt (optionally with reference images embedded as vision
940
+ tokens) into stacked Qwen3-VL hidden states of shape `(seq_len, num_text_layers,
941
+ text_hidden_dim)` at natural (unpadded) length."""
942
+ device = device or self._execution_device
943
+ prefix_idx = self.prompt_template_encode_start_idx
944
+
945
+ # The suffix is tokenized separately so it lands after the prompt tokens.
946
+ suffix_inputs = self.tokenizer([self.prompt_template_encode_suffix], return_tensors="pt").to(device)
947
+ suffix_ids = suffix_inputs["input_ids"]
948
+ suffix_mask = suffix_inputs["attention_mask"].bool()
949
+
950
+ extra_inputs = {}
951
+ if images:
952
+ # Reference images ride in the user message ahead of the prompt via named
953
+ # vision placeholders; the processor expands each <|image_pad|> to the
954
+ # image's token grid.
955
+ image_prompt = "".join(
956
+ f"Picture {i + 1}: <|vision_start|><|image_pad|><|vision_end|>" for i in range(len(images))
957
+ )
958
+ text = self.prompt_template_encode_prefix + image_prompt + prompt
959
+ # No truncation here: the expanded image-pad runs must stay intact.
960
+ inputs = self.vl_processor(text=[text], images=list(images), return_tensors="pt", do_rescale=False).to(
961
+ device
962
+ )
963
+ for k, v in inputs.items():
964
+ if k in ("input_ids", "attention_mask"):
965
+ continue
966
+ if isinstance(v, torch.Tensor) and v.is_floating_point():
967
+ v = v.to(self.text_encoder.dtype)
968
+ extra_inputs[k] = v
969
+ else:
970
+ text = self.prompt_template_encode_prefix + prompt
971
+ inputs = self.tokenizer(
972
+ [text], truncation=True, max_length=max_sequence_length + prefix_idx, return_tensors="pt"
973
+ ).to(device)
974
+
975
+ input_ids = torch.cat([inputs["input_ids"], suffix_ids], dim=1)
976
+ attention_mask = torch.cat([inputs["attention_mask"].bool(), suffix_mask], dim=1)
977
+
978
+ # mm_token_type_ids (used for M-RoPE) must cover the appended suffix tokens
979
+ # too; they are plain text -> type 0.
980
+ if "mm_token_type_ids" in extra_inputs:
981
+ tt = extra_inputs["mm_token_type_ids"]
982
+ extra_inputs["mm_token_type_ids"] = torch.cat(
983
+ [tt, torch.zeros_like(suffix_ids, dtype=tt.dtype)], dim=1
984
+ )
985
+
986
+ outputs = self.text_encoder(
987
+ input_ids=input_ids,
988
+ attention_mask=attention_mask,
989
+ output_hidden_states=True,
990
+ **extra_inputs,
991
+ )
992
+
993
+ hidden_states = torch.stack([outputs.hidden_states[i] for i in self.text_encoder_select_layers], dim=2)
994
+ # Drop the system-prefix tokens; what remains is (image +) prompt + suffix.
995
+ return hidden_states[0, prefix_idx:]
996
+
997
+ def encode_prompt(
998
+ self,
999
+ prompt: Union[str, List[str]],
1000
+ images: Optional[List[torch.Tensor]] = None,
1001
+ num_images_per_prompt: int = 1,
1002
+ max_sequence_length: int = 512,
1003
+ device: Optional[torch.device] = None,
1004
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
1005
+ """Encode prompts (all sharing the same reference images, if any) and right-pad
1006
+ them into a batch. Returns `(prompt_embeds, prompt_embeds_mask)` of shapes
1007
+ `(B, L, num_text_layers, D)` and `(B, L)` (bool)."""
1008
+ device = device or self._execution_device
1009
+ prompt = [prompt] if isinstance(prompt, str) else prompt
1010
+
1011
+ features = [self._encode_single_prompt(p, images, max_sequence_length, device) for p in prompt]
1012
+ max_len = max(f.shape[0] for f in features)
1013
+ embeds = features[0].new_zeros(len(features), max_len, *features[0].shape[1:])
1014
+ mask = torch.zeros(len(features), max_len, dtype=torch.bool, device=device)
1015
+ for i, f in enumerate(features):
1016
+ embeds[i, : f.shape[0]] = f
1017
+ mask[i, : f.shape[0]] = True
1018
+
1019
+ embeds = embeds.repeat_interleave(num_images_per_prompt, dim=0)
1020
+ mask = mask.repeat_interleave(num_images_per_prompt, dim=0)
1021
+ return embeds, mask
1022
+
1023
+ # ------------------------------------------------------------------
1024
+ # Latent packing helpers
1025
+ # ------------------------------------------------------------------
1026
+ def _pack_latents(self, latents: torch.Tensor) -> torch.Tensor:
1027
+ """(B, C, H, W) latents -> (B, H/p * W/p, C * p * p) tokens."""
1028
+ b, c, h, w = latents.shape
1029
+ p = self.patch_size
1030
+ latents = latents.view(b, c, h // p, p, w // p, p)
1031
+ latents = latents.permute(0, 2, 4, 1, 3, 5)
1032
+ return latents.reshape(b, (h // p) * (w // p), c * p * p)
1033
+
1034
+ def _unpack_latents(self, latents: torch.Tensor, height: int, width: int) -> torch.Tensor:
1035
+ """(B, L, C * p * p) tokens -> (B, C, 1, H, W) latents (frame dim for the VAE)."""
1036
+ batch_size, _, channels = latents.shape
1037
+ p = self.patch_size
1038
+ h = p * (int(height) // (self.vae_scale_factor * p))
1039
+ w = p * (int(width) // (self.vae_scale_factor * p))
1040
+ latents = latents.view(batch_size, h // p, w // p, channels // (p * p), p, p)
1041
+ latents = latents.permute(0, 3, 1, 4, 2, 5)
1042
+ return latents.reshape(batch_size, channels // (p * p), 1, h, w)
1043
+
1044
+ def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None):
1045
+ if latents is not None:
1046
+ return latents.to(device=device, dtype=dtype)
1047
+ shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor)
1048
+ if isinstance(generator, list) and len(generator) != batch_size:
1049
+ raise ValueError(
1050
+ f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
1051
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
1052
+ )
1053
+ latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
1054
+ return self._pack_latents(latents)
1055
+
1056
+ def _encode_reference_latents(
1057
+ self,
1058
+ images: List[torch.Tensor],
1059
+ max_pixels: int,
1060
+ generator: Optional[torch.Generator],
1061
+ device: torch.device,
1062
+ ) -> List[torch.Tensor]:
1063
+ """Encode `[0, 1]` CHW reference images to normalized VAE latents, one `(C, h, w)`
1064
+ tensor per image. Each image is downscaled (aspect-preserving, never upscaled) to
1065
+ fit within `max_pixels`, then snapped so the latent grid is patchifiable."""
1066
+ snap = self.vae_scale_factor * self.patch_size
1067
+ vae_dtype = self.vae.dtype
1068
+
1069
+ latents_mean = torch.tensor(self.vae.config.latents_mean).view(1, self.vae.config.z_dim, 1, 1, 1)
1070
+ latents_std = torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1)
1071
+
1072
+ ref_latents = []
1073
+ for img in images:
1074
+ img = img.unsqueeze(0).to(device, dtype=vae_dtype)
1075
+ h, w = img.shape[2], img.shape[3]
1076
+ if h * w > max_pixels:
1077
+ ratio = h / w
1078
+ new_h, new_w = math.sqrt(max_pixels * ratio), math.sqrt(max_pixels / ratio)
1079
+ else:
1080
+ new_h, new_w = float(h), float(w)
1081
+ new_h = max(snap, int(round(new_h / snap)) * snap)
1082
+ new_w = max(snap, int(round(new_w / snap)) * snap)
1083
+ if (new_h, new_w) != (h, w):
1084
+ img = F.interpolate(img.float(), size=(new_h, new_w), mode="bilinear").to(vae_dtype)
1085
+
1086
+ img = (img * 2.0 - 1.0).unsqueeze(2) # [0,1] -> [-1,1], add frame dim
1087
+ latent = self.vae.encode(img).latent_dist.sample(generator)
1088
+ latent = (latent - latents_mean.to(latent.device, latent.dtype)) / latents_std.to(
1089
+ latent.device, latent.dtype
1090
+ )
1091
+ ref_latents.append(latent[:, :, 0][0]) # drop frame + batch dims -> (C, h, w)
1092
+ return ref_latents
1093
+
1094
+ def _pack_reference_latents(
1095
+ self,
1096
+ ref_latents: List[torch.Tensor],
1097
+ device: torch.device,
1098
+ dtype: torch.dtype,
1099
+ placements: Optional[List[Dict[str, Any]]] = None,
1100
+ target_grid_size: Optional[Tuple[int, int]] = None,
1101
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
1102
+ """Patchify reference latents into `(1, ref_seq_len, C * p * p)` tokens and build
1103
+ their `(ref_seq_len, 3)` rotary coordinates. The i-th reference sits on frame
1104
+ axis `i + 1` with its own y/x grid starting at 0 (Kontext "index" placement)."""
1105
+ p = self.patch_size
1106
+ tokens, position_ids = [], []
1107
+ for i, ref in enumerate(ref_latents):
1108
+ ref = ref.unsqueeze(0).to(device, dtype)
1109
+ tokens.append(self._pack_latents(ref))
1110
+ _, _, h, w = ref.shape
1111
+ ids = torch.zeros(h // p, w // p, 3, device=device)
1112
+ ids[..., 0] = i + 1
1113
+ placement = placements[i] if placements is not None else None
1114
+ if placement is None:
1115
+ ids[..., 1] = torch.arange(h // p, device=device)[:, None]
1116
+ ids[..., 2] = torch.arange(w // p, device=device)[None, :]
1117
+ else:
1118
+ if target_grid_size is None:
1119
+ raise ValueError("`target_grid_size` is required for registered references.")
1120
+ bbox = placement.get("bbox_normalized")
1121
+ if bbox is None or len(bbox) != 4:
1122
+ raise ValueError("A registered reference requires bbox_normalized=[x0,y0,x1,y1].")
1123
+ x0, y0, x1, y1 = (float(value) for value in bbox)
1124
+ if not (0 <= x0 < x1 <= 1 and 0 <= y0 < y1 <= 1):
1125
+ raise ValueError(f"Invalid normalized reference bbox: {bbox}")
1126
+ target_h, target_w = target_grid_size
1127
+ ref_h, ref_w = h // p, w // p
1128
+ ys = y0 * target_h + (torch.arange(ref_h, device=device) + 0.5) * (
1129
+ (y1 - y0) * target_h / ref_h
1130
+ ) - 0.5
1131
+ xs = x0 * target_w + (torch.arange(ref_w, device=device) + 0.5) * (
1132
+ (x1 - x0) * target_w / ref_w
1133
+ ) - 0.5
1134
+ ids[..., 1] = ys[:, None]
1135
+ ids[..., 2] = xs[None, :]
1136
+ position_ids.append(ids.reshape(-1, 3))
1137
+ return torch.cat(tokens, dim=1), torch.cat(position_ids, dim=0)
1138
+
1139
+ @staticmethod
1140
+ def prepare_position_ids(text_seq_len: int, grid_height: int, grid_width: int, device: torch.device):
1141
+ """Rotary coordinates for the `[text, image]` sequence: text tokens sit at the
1142
+ origin, image tokens carry their `(0, h, w)` latent-grid coordinates."""
1143
+ text_ids = torch.zeros(text_seq_len, 3, device=device)
1144
+ image_ids = torch.zeros(grid_height, grid_width, 3, device=device)
1145
+ image_ids[..., 1] = torch.arange(grid_height, device=device)[:, None]
1146
+ image_ids[..., 2] = torch.arange(grid_width, device=device)[None, :]
1147
+ image_ids = image_ids.reshape(grid_height * grid_width, 3)
1148
+ return torch.cat([text_ids, image_ids], dim=0)
1149
+
1150
+ # ------------------------------------------------------------------
1151
+ # LoRA loading (Ostris AI-Toolkit / ComfyUI / diffusers formats)
1152
+ # ------------------------------------------------------------------
1153
+ def load_lora_weights(
1154
+ self,
1155
+ pretrained_model_name_or_path_or_dict: Union[str, Dict[str, torch.Tensor]],
1156
+ weight_name: Optional[str] = None,
1157
+ adapter_name: str = "default",
1158
+ **kwargs,
1159
+ ):
1160
+ r"""
1161
+ Load a Krea 2 LoRA into the transformer.
1162
+
1163
+ Accepts a state dict, a local `.safetensors` file or directory, or a hub repo id
1164
+ (with `weight_name` selecting the file when the repo holds several). Handles
1165
+ Ostris AI-Toolkit / ComfyUI key layouts (`diffusion_model.blocks...` with
1166
+ `lora_A`/`lora_B` or `lora_down`/`lora_up`) as well as already-converted
1167
+ diffusers-format state dicts (`transformer.transformer_blocks...`).
1168
+ """
1169
+ if isinstance(pretrained_model_name_or_path_or_dict, dict):
1170
+ state_dict = dict(pretrained_model_name_or_path_or_dict)
1171
+ else:
1172
+ from safetensors.torch import load_file
1173
+
1174
+ path = str(pretrained_model_name_or_path_or_dict)
1175
+ if os.path.isfile(path):
1176
+ file_path = path
1177
+ elif os.path.isdir(path):
1178
+ if weight_name is None:
1179
+ candidates = [f for f in os.listdir(path) if f.endswith(".safetensors")]
1180
+ if len(candidates) != 1:
1181
+ raise ValueError(
1182
+ f"Could not pick a LoRA file in {path}: found {candidates}. Pass `weight_name`."
1183
+ )
1184
+ weight_name = candidates[0]
1185
+ file_path = os.path.join(path, weight_name)
1186
+ else:
1187
+ from huggingface_hub import hf_hub_download, list_repo_files
1188
+
1189
+ if weight_name is None:
1190
+ candidates = [
1191
+ f for f in list_repo_files(path, token=kwargs.get("token", None)) if f.endswith(".safetensors")
1192
+ ]
1193
+ if len(candidates) != 1:
1194
+ raise ValueError(
1195
+ f"Could not pick a LoRA file in hub repo {path}: found {candidates}. Pass `weight_name`."
1196
+ )
1197
+ weight_name = candidates[0]
1198
+ file_path = hf_hub_download(path, weight_name, token=kwargs.get("token", None))
1199
+ state_dict = load_file(file_path)
1200
+
1201
+ state_dict = _normalize_lora_state_dict(state_dict)
1202
+ if not any(k.startswith("transformer.") for k in state_dict):
1203
+ state_dict = _convert_non_diffusers_krea2_lora_to_diffusers(state_dict)
1204
+
1205
+ self.transformer.load_lora_adapter(state_dict, prefix="transformer", adapter_name=adapter_name)
1206
+
1207
+ def unload_lora_weights(self):
1208
+ """Remove all loaded LoRA adapters from the transformer."""
1209
+ transformer = self.transformer
1210
+ if hasattr(transformer, "unload_lora"):
1211
+ transformer.unload_lora()
1212
+ elif getattr(transformer, "peft_config", None):
1213
+ transformer.delete_adapters(list(transformer.peft_config.keys()))
1214
+
1215
+ def fuse_lora(self, lora_scale: float = 1.0, adapter_names: Optional[List[str]] = None, **kwargs):
1216
+ """Fuse the loaded LoRA weights into the transformer for adapter-free inference."""
1217
+ self.transformer.fuse_lora(lora_scale=lora_scale, adapter_names=adapter_names, **kwargs)
1218
+
1219
+ def unfuse_lora(self, **kwargs):
1220
+ self.transformer.unfuse_lora(**kwargs)
1221
+
1222
+ def set_adapters(self, adapter_names: Union[str, List[str]], weights: Optional[Union[float, List[float]]] = None):
1223
+ """Activate (and optionally weight) specific loaded LoRA adapters."""
1224
+ self.transformer.set_adapters(adapter_names, weights)
1225
+
1226
+ # ------------------------------------------------------------------
1227
+ # Generation
1228
+ # ------------------------------------------------------------------
1229
+ @property
1230
+ def guidance_scale(self):
1231
+ return self._guidance_scale
1232
+
1233
+ @property
1234
+ def do_classifier_free_guidance(self):
1235
+ return self._guidance_scale > 0
1236
+
1237
+ @torch.no_grad()
1238
+ def __call__(
1239
+ self,
1240
+ prompt: Union[str, List[str], None] = None,
1241
+ image: Union[PIL.Image.Image, np.ndarray, torch.Tensor, List, None] = None,
1242
+ negative_prompt: Union[str, List[str], None] = None,
1243
+ height: int = 1024,
1244
+ width: int = 1024,
1245
+ num_inference_steps: Optional[int] = None,
1246
+ sigmas: Optional[List[float]] = None,
1247
+ guidance_scale: Optional[float] = None,
1248
+ num_images_per_prompt: int = 1,
1249
+ generator: Union[torch.Generator, List[torch.Generator], None] = None,
1250
+ latents: Optional[torch.Tensor] = None,
1251
+ prompt_embeds: Optional[torch.Tensor] = None,
1252
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
1253
+ negative_prompt_embeds: Optional[torch.Tensor] = None,
1254
+ negative_prompt_embeds_mask: Optional[torch.Tensor] = None,
1255
+ reference_max_pixels: int = 1024 * 1024,
1256
+ reference_placements: Optional[List[Dict[str, Any]]] = None,
1257
+ vl_image_max_pixels: int = 384 * 384,
1258
+ encode_reference_in_prompt: bool = True,
1259
+ kv_cache: bool = False,
1260
+ output_type: Optional[str] = "pil",
1261
+ return_dict: bool = True,
1262
+ attention_kwargs: Optional[Dict[str, Any]] = None,
1263
+ max_sequence_length: int = 512,
1264
+ ):
1265
+ r"""
1266
+ Generate images from a prompt, optionally conditioned on reference images.
1267
+
1268
+ Args:
1269
+ prompt (`str` or `list[str]`):
1270
+ The prompt(s) to guide generation. For edits, describe the change (e.g.
1271
+ "make the sky purple").
1272
+ image (`PIL.Image.Image`, `np.ndarray`, `torch.Tensor` or a list of them, *optional*):
1273
+ Reference image(s). They are encoded into the prompt conditioning via
1274
+ the Qwen3-VL vision tower and appended to the transformer sequence as
1275
+ clean VAE latents at t=0. References keep their own aspect ratio; the
1276
+ output size is set by `height`/`width` independently.
1277
+ negative_prompt (`str` or `list[str]`, *optional*):
1278
+ Prompt(s) not to guide generation; ignored when `guidance_scale <= 0`.
1279
+ height / width (`int`, defaults to 1024):
1280
+ Output size in pixels; rounded up to a multiple of 16 if needed.
1281
+ num_inference_steps (`int`, *optional*):
1282
+ Denoising steps. Defaults to 8 for a distilled (Turbo) checkpoint and 28
1283
+ otherwise.
1284
+ sigmas (`list[float]`, *optional*):
1285
+ Custom sigma grid for the scheduler.
1286
+ guidance_scale (`float`, *optional*):
1287
+ Krea 2 CFG convention: velocity is `cond + scale * (cond - uncond)` and
1288
+ guidance is enabled whenever `scale > 0` (equals standard CFG with scale
1289
+ `1 + scale`). Defaults to 0.0 for a distilled checkpoint and 4.5
1290
+ otherwise.
1291
+ num_images_per_prompt (`int`, defaults to 1):
1292
+ Number of images per prompt.
1293
+ generator (`torch.Generator` or `list[torch.Generator]`, *optional*):
1294
+ RNG for deterministic generation.
1295
+ latents (`torch.Tensor`, *optional*):
1296
+ Pre-generated packed noisy latents `(B, image_seq_len, in_channels)`.
1297
+ prompt_embeds / prompt_embeds_mask (`torch.Tensor`, *optional*):
1298
+ Pre-computed text conditioning `(B, L, num_text_layers, D)` and its
1299
+ bool mask `(B, L)`; skips prompt encoding when given.
1300
+ negative_prompt_embeds / negative_prompt_embeds_mask (`torch.Tensor`, *optional*):
1301
+ Same, for the negative prompt.
1302
+ reference_max_pixels (`int`, defaults to `1024 * 1024`):
1303
+ Pixel budget each reference image is downscaled to fit before VAE
1304
+ encoding (never upscaled).
1305
+ vl_image_max_pixels (`int`, defaults to `384 * 384`):
1306
+ Pixel budget for the (coarse) Qwen3-VL view of each reference image.
1307
+ encode_reference_in_prompt (`bool`, defaults to `True`):
1308
+ Whether reference images are also embedded into the text conditioning
1309
+ through the Qwen3-VL vision tower (matches AI-Toolkit edit training).
1310
+ kv_cache (`bool`, defaults to `False`):
1311
+ Cache the reference tokens' attention K/V: they are precomputed
1312
+ in a single t=0 pass and reused on every denoising step, so the
1313
+ reference tokens never ride along in the per-step sequence --
1314
+ faster, especially with CFG or many steps. The LoRA must be
1315
+ trained with AI-Toolkit's ``kv_cache`` model kwarg (reference
1316
+ tokens attend only to each other) for this to work properly;
1317
+ leave off for normally trained edit LoRAs.
1318
+ output_type (`str`, defaults to `"pil"`):
1319
+ `"pil"`, `"np"`, `"pt"` or `"latent"`.
1320
+ return_dict (`bool`, defaults to `True`):
1321
+ Whether to return a [`Krea2PipelineOutput`] instead of a plain tuple.
1322
+ attention_kwargs (`dict`, *optional*):
1323
+ Forwarded to the transformer; a `scale` entry sets the LoRA scale.
1324
+ max_sequence_length (`int`, defaults to 512):
1325
+ Maximum prompt token length (truncation only; no fixed padding).
1326
+
1327
+ Returns:
1328
+ [`Krea2PipelineOutput`] or `tuple`: the generated images.
1329
+ """
1330
+ if num_inference_steps is None:
1331
+ num_inference_steps = 8 if self.config.is_distilled else 28
1332
+ if guidance_scale is None:
1333
+ guidance_scale = 0.0 if self.config.is_distilled else 4.5
1334
+
1335
+ multiple = self.vae_scale_factor * self.patch_size
1336
+ if height % multiple != 0 or width % multiple != 0:
1337
+ rounded_height = ((height + multiple - 1) // multiple) * multiple
1338
+ rounded_width = ((width + multiple - 1) // multiple) * multiple
1339
+ logger.warning(
1340
+ f"`height` and `width` must be multiples of {multiple}; rounding up from {height}x{width} to"
1341
+ f" {rounded_height}x{rounded_width}."
1342
+ )
1343
+ height, width = rounded_height, rounded_width
1344
+
1345
+ if prompt is None and prompt_embeds is None:
1346
+ raise ValueError("Provide either `prompt` or `prompt_embeds`.")
1347
+ if prompt_embeds is not None and prompt_embeds_mask is None:
1348
+ raise ValueError("`prompt_embeds` requires `prompt_embeds_mask`.")
1349
+ if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
1350
+ raise ValueError("`negative_prompt_embeds` requires `negative_prompt_embeds_mask`.")
1351
+
1352
+ self._guidance_scale = guidance_scale
1353
+
1354
+ if prompt is not None and isinstance(prompt, str):
1355
+ batch_size = 1
1356
+ elif prompt is not None:
1357
+ batch_size = len(prompt)
1358
+ else:
1359
+ batch_size = prompt_embeds.shape[0]
1360
+
1361
+ device = self._execution_device
1362
+ transformer_dtype = self.transformer.dtype
1363
+
1364
+ # 1. Normalize reference images to a list of [0, 1] CHW tensors.
1365
+ ref_images = None
1366
+ if image is not None:
1367
+ image_list = image if isinstance(image, (list, tuple)) else [image]
1368
+ ref_images = [self._to_chw_tensor(img) for img in image_list]
1369
+
1370
+ # 2. Encode the prompt(s). With references, the coarse VL view of each image is
1371
+ # embedded in the user message so the text conditioning "sees" them.
1372
+ vl_images = None
1373
+ if ref_images is not None and encode_reference_in_prompt:
1374
+ vl_images = self._prep_vl_images([img.to(device) for img in ref_images], vl_image_max_pixels)
1375
+
1376
+ if prompt_embeds is None:
1377
+ prompt_embeds, prompt_embeds_mask = self.encode_prompt(
1378
+ prompt, vl_images, num_images_per_prompt, max_sequence_length, device
1379
+ )
1380
+ prompt_embeds = prompt_embeds.to(transformer_dtype)
1381
+
1382
+ if self.do_classifier_free_guidance:
1383
+ if negative_prompt_embeds is None:
1384
+ negative_prompt = negative_prompt if negative_prompt is not None else ""
1385
+ if isinstance(negative_prompt, str):
1386
+ negative_prompt = [negative_prompt] * batch_size
1387
+ negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
1388
+ negative_prompt, vl_images, num_images_per_prompt, max_sequence_length, device
1389
+ )
1390
+ negative_prompt_embeds = negative_prompt_embeds.to(transformer_dtype)
1391
+
1392
+ # 3. Prepare the noisy latents (kept in float32 across scheduler steps).
1393
+ num_channels_latents = self.transformer.config.in_channels // (self.patch_size**2)
1394
+ latents = self.prepare_latents(
1395
+ batch_size * num_images_per_prompt,
1396
+ num_channels_latents,
1397
+ height,
1398
+ width,
1399
+ torch.float32,
1400
+ device,
1401
+ generator,
1402
+ latents,
1403
+ )
1404
+ grid_height = height // (self.vae_scale_factor * self.patch_size)
1405
+ grid_width = width // (self.vae_scale_factor * self.patch_size)
1406
+
1407
+ # 4. Encode + pack reference latents (shared across the batch) and build the
1408
+ # combined rotary coordinates.
1409
+ ref_tokens, ref_seq_len = None, 0
1410
+ neg_position_ids = None
1411
+ position_ids = self.prepare_position_ids(prompt_embeds.shape[1], grid_height, grid_width, device)
1412
+ if self.do_classifier_free_guidance:
1413
+ neg_position_ids = self.prepare_position_ids(
1414
+ negative_prompt_embeds.shape[1], grid_height, grid_width, device
1415
+ )
1416
+ ref_kv = None
1417
+ if ref_images is not None:
1418
+ if reference_placements is not None and len(reference_placements) != len(ref_images):
1419
+ raise ValueError("`reference_placements` must match the number of reference images.")
1420
+ ref_latents = self._encode_reference_latents(ref_images, reference_max_pixels, generator, device)
1421
+ ref_tokens, ref_position_ids = self._pack_reference_latents(
1422
+ ref_latents,
1423
+ device,
1424
+ transformer_dtype,
1425
+ placements=reference_placements,
1426
+ target_grid_size=(grid_height, grid_width),
1427
+ )
1428
+ ref_seq_len = ref_tokens.shape[1]
1429
+ ref_tokens = ref_tokens.expand(latents.shape[0], -1, -1)
1430
+ if kv_cache:
1431
+ # Precompute pass: the refs alone run through the blocks once at t=0
1432
+ # and every denoising step reuses their per-block K/V, so the ref
1433
+ # tokens are dropped from the per-step sequence entirely.
1434
+ ref_kv = self.transformer.precompute_ref_kv(ref_tokens, ref_position_ids, attention_kwargs)
1435
+ ref_tokens, ref_seq_len = None, 0
1436
+ else:
1437
+ position_ids = torch.cat([position_ids, ref_position_ids], dim=0)
1438
+ if neg_position_ids is not None:
1439
+ neg_position_ids = torch.cat([neg_position_ids, ref_position_ids], dim=0)
1440
+
1441
+ # 5. Prepare timesteps. The distilled (Turbo) checkpoint was trained at a fixed
1442
+ # exponential time shift mu=1.15; the base checkpoint interpolates mu from the
1443
+ # image token count.
1444
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
1445
+ if self.config.is_distilled:
1446
+ mu = 1.15
1447
+ else:
1448
+ mu = calculate_shift(
1449
+ grid_height * grid_width,
1450
+ self.scheduler.config.get("base_image_seq_len", 256),
1451
+ self.scheduler.config.get("max_image_seq_len", 6400),
1452
+ self.scheduler.config.get("base_shift", 0.5),
1453
+ self.scheduler.config.get("max_shift", 1.15),
1454
+ )
1455
+ self.scheduler.set_timesteps(sigmas=sigmas, device=device, mu=mu)
1456
+ timesteps = self.scheduler.timesteps
1457
+ self.scheduler.set_begin_index(0)
1458
+
1459
+ # 6. Denoising loop (Euler flow ODE integration via the scheduler).
1460
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
1461
+ for t in timesteps:
1462
+ timestep = (t / self.scheduler.config.num_train_timesteps).expand(latents.shape[0]).to(
1463
+ transformer_dtype
1464
+ )
1465
+
1466
+ model_input = latents.to(transformer_dtype)
1467
+ if ref_tokens is not None:
1468
+ model_input = torch.cat([model_input, ref_tokens], dim=1)
1469
+
1470
+ noise_pred = self.transformer(
1471
+ hidden_states=model_input,
1472
+ encoder_hidden_states=prompt_embeds,
1473
+ timestep=timestep,
1474
+ position_ids=position_ids,
1475
+ encoder_attention_mask=prompt_embeds_mask,
1476
+ ref_seq_len=ref_seq_len,
1477
+ ref_kv_cache=ref_kv,
1478
+ attention_kwargs=attention_kwargs,
1479
+ return_dict=False,
1480
+ )[0]
1481
+
1482
+ if self.do_classifier_free_guidance:
1483
+ neg_noise_pred = self.transformer(
1484
+ hidden_states=model_input,
1485
+ encoder_hidden_states=negative_prompt_embeds,
1486
+ timestep=timestep,
1487
+ position_ids=neg_position_ids,
1488
+ encoder_attention_mask=negative_prompt_embeds_mask,
1489
+ ref_seq_len=ref_seq_len,
1490
+ ref_kv_cache=ref_kv,
1491
+ attention_kwargs=attention_kwargs,
1492
+ return_dict=False,
1493
+ )[0]
1494
+ noise_pred = noise_pred + guidance_scale * (noise_pred - neg_noise_pred)
1495
+
1496
+ latents = self.scheduler.step(noise_pred.float(), t, latents, return_dict=False)[0]
1497
+ progress_bar.update()
1498
+
1499
+ # 7. Decode latents.
1500
+ if output_type == "latent":
1501
+ image_out = latents
1502
+ else:
1503
+ latents = self._unpack_latents(latents, height, width).to(self.vae.dtype)
1504
+ latents_mean = (
1505
+ torch.tensor(self.vae.config.latents_mean)
1506
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
1507
+ .to(latents.device, latents.dtype)
1508
+ )
1509
+ latents_std = (
1510
+ torch.tensor(self.vae.config.latents_std)
1511
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
1512
+ .to(latents.device, latents.dtype)
1513
+ )
1514
+ latents = latents * latents_std + latents_mean
1515
+ image_out = self.vae.decode(latents, return_dict=False)[0][:, :, 0]
1516
+ image_out = self.image_processor.postprocess(image_out, output_type=output_type)
1517
+
1518
+ self.maybe_free_model_hooks()
1519
+
1520
+ if not return_dict:
1521
+ return (image_out,)
1522
+ return Krea2PipelineOutput(images=image_out)