hatushimo ostris commited on
Commit
1a9a404
·
0 Parent(s):

Duplicate from ostris/Krea2OstrisEdit

Browse files

Co-authored-by: Ostris <ostris@users.noreply.huggingface.co>

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

  • SHA256: dd36b3495e36eb94460f9d6d403ea6b9926244ab3f940074a9a214f92e68fc69
  • Pointer size: 132 Bytes
  • Size of remote file: 2.09 MB
pipeline.py ADDED
@@ -0,0 +1,1487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, ref_latents: List[torch.Tensor], device: torch.device, dtype: torch.dtype
1096
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
1097
+ """Patchify reference latents into `(1, ref_seq_len, C * p * p)` tokens and build
1098
+ their `(ref_seq_len, 3)` rotary coordinates. The i-th reference sits on frame
1099
+ axis `i + 1` with its own y/x grid starting at 0 (Kontext "index" placement)."""
1100
+ p = self.patch_size
1101
+ tokens, position_ids = [], []
1102
+ for i, ref in enumerate(ref_latents):
1103
+ ref = ref.unsqueeze(0).to(device, dtype)
1104
+ tokens.append(self._pack_latents(ref))
1105
+ _, _, h, w = ref.shape
1106
+ ids = torch.zeros(h // p, w // p, 3, device=device)
1107
+ ids[..., 0] = i + 1
1108
+ ids[..., 1] = torch.arange(h // p, device=device)[:, None]
1109
+ ids[..., 2] = torch.arange(w // p, device=device)[None, :]
1110
+ position_ids.append(ids.reshape(-1, 3))
1111
+ return torch.cat(tokens, dim=1), torch.cat(position_ids, dim=0)
1112
+
1113
+ @staticmethod
1114
+ def prepare_position_ids(text_seq_len: int, grid_height: int, grid_width: int, device: torch.device):
1115
+ """Rotary coordinates for the `[text, image]` sequence: text tokens sit at the
1116
+ origin, image tokens carry their `(0, h, w)` latent-grid coordinates."""
1117
+ text_ids = torch.zeros(text_seq_len, 3, device=device)
1118
+ image_ids = torch.zeros(grid_height, grid_width, 3, device=device)
1119
+ image_ids[..., 1] = torch.arange(grid_height, device=device)[:, None]
1120
+ image_ids[..., 2] = torch.arange(grid_width, device=device)[None, :]
1121
+ image_ids = image_ids.reshape(grid_height * grid_width, 3)
1122
+ return torch.cat([text_ids, image_ids], dim=0)
1123
+
1124
+ # ------------------------------------------------------------------
1125
+ # LoRA loading (Ostris AI-Toolkit / ComfyUI / diffusers formats)
1126
+ # ------------------------------------------------------------------
1127
+ def load_lora_weights(
1128
+ self,
1129
+ pretrained_model_name_or_path_or_dict: Union[str, Dict[str, torch.Tensor]],
1130
+ weight_name: Optional[str] = None,
1131
+ adapter_name: str = "default",
1132
+ **kwargs,
1133
+ ):
1134
+ r"""
1135
+ Load a Krea 2 LoRA into the transformer.
1136
+
1137
+ Accepts a state dict, a local `.safetensors` file or directory, or a hub repo id
1138
+ (with `weight_name` selecting the file when the repo holds several). Handles
1139
+ Ostris AI-Toolkit / ComfyUI key layouts (`diffusion_model.blocks...` with
1140
+ `lora_A`/`lora_B` or `lora_down`/`lora_up`) as well as already-converted
1141
+ diffusers-format state dicts (`transformer.transformer_blocks...`).
1142
+ """
1143
+ if isinstance(pretrained_model_name_or_path_or_dict, dict):
1144
+ state_dict = dict(pretrained_model_name_or_path_or_dict)
1145
+ else:
1146
+ from safetensors.torch import load_file
1147
+
1148
+ path = str(pretrained_model_name_or_path_or_dict)
1149
+ if os.path.isfile(path):
1150
+ file_path = path
1151
+ elif os.path.isdir(path):
1152
+ if weight_name is None:
1153
+ candidates = [f for f in os.listdir(path) if f.endswith(".safetensors")]
1154
+ if len(candidates) != 1:
1155
+ raise ValueError(
1156
+ f"Could not pick a LoRA file in {path}: found {candidates}. Pass `weight_name`."
1157
+ )
1158
+ weight_name = candidates[0]
1159
+ file_path = os.path.join(path, weight_name)
1160
+ else:
1161
+ from huggingface_hub import hf_hub_download, list_repo_files
1162
+
1163
+ if weight_name is None:
1164
+ candidates = [
1165
+ f for f in list_repo_files(path, token=kwargs.get("token", None)) if f.endswith(".safetensors")
1166
+ ]
1167
+ if len(candidates) != 1:
1168
+ raise ValueError(
1169
+ f"Could not pick a LoRA file in hub repo {path}: found {candidates}. Pass `weight_name`."
1170
+ )
1171
+ weight_name = candidates[0]
1172
+ file_path = hf_hub_download(path, weight_name, token=kwargs.get("token", None))
1173
+ state_dict = load_file(file_path)
1174
+
1175
+ state_dict = _normalize_lora_state_dict(state_dict)
1176
+ if not any(k.startswith("transformer.") for k in state_dict):
1177
+ state_dict = _convert_non_diffusers_krea2_lora_to_diffusers(state_dict)
1178
+
1179
+ self.transformer.load_lora_adapter(state_dict, prefix="transformer", adapter_name=adapter_name)
1180
+
1181
+ def unload_lora_weights(self):
1182
+ """Remove all loaded LoRA adapters from the transformer."""
1183
+ transformer = self.transformer
1184
+ if hasattr(transformer, "unload_lora"):
1185
+ transformer.unload_lora()
1186
+ elif getattr(transformer, "peft_config", None):
1187
+ transformer.delete_adapters(list(transformer.peft_config.keys()))
1188
+
1189
+ def fuse_lora(self, lora_scale: float = 1.0, adapter_names: Optional[List[str]] = None, **kwargs):
1190
+ """Fuse the loaded LoRA weights into the transformer for adapter-free inference."""
1191
+ self.transformer.fuse_lora(lora_scale=lora_scale, adapter_names=adapter_names, **kwargs)
1192
+
1193
+ def unfuse_lora(self, **kwargs):
1194
+ self.transformer.unfuse_lora(**kwargs)
1195
+
1196
+ def set_adapters(self, adapter_names: Union[str, List[str]], weights: Optional[Union[float, List[float]]] = None):
1197
+ """Activate (and optionally weight) specific loaded LoRA adapters."""
1198
+ self.transformer.set_adapters(adapter_names, weights)
1199
+
1200
+ # ------------------------------------------------------------------
1201
+ # Generation
1202
+ # ------------------------------------------------------------------
1203
+ @property
1204
+ def guidance_scale(self):
1205
+ return self._guidance_scale
1206
+
1207
+ @property
1208
+ def do_classifier_free_guidance(self):
1209
+ return self._guidance_scale > 0
1210
+
1211
+ @torch.no_grad()
1212
+ def __call__(
1213
+ self,
1214
+ prompt: Union[str, List[str], None] = None,
1215
+ image: Union[PIL.Image.Image, np.ndarray, torch.Tensor, List, None] = None,
1216
+ negative_prompt: Union[str, List[str], None] = None,
1217
+ height: int = 1024,
1218
+ width: int = 1024,
1219
+ num_inference_steps: Optional[int] = None,
1220
+ sigmas: Optional[List[float]] = None,
1221
+ guidance_scale: Optional[float] = None,
1222
+ num_images_per_prompt: int = 1,
1223
+ generator: Union[torch.Generator, List[torch.Generator], None] = None,
1224
+ latents: Optional[torch.Tensor] = None,
1225
+ prompt_embeds: Optional[torch.Tensor] = None,
1226
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
1227
+ negative_prompt_embeds: Optional[torch.Tensor] = None,
1228
+ negative_prompt_embeds_mask: Optional[torch.Tensor] = None,
1229
+ reference_max_pixels: int = 1024 * 1024,
1230
+ vl_image_max_pixels: int = 384 * 384,
1231
+ encode_reference_in_prompt: bool = True,
1232
+ kv_cache: bool = False,
1233
+ output_type: Optional[str] = "pil",
1234
+ return_dict: bool = True,
1235
+ attention_kwargs: Optional[Dict[str, Any]] = None,
1236
+ max_sequence_length: int = 512,
1237
+ ):
1238
+ r"""
1239
+ Generate images from a prompt, optionally conditioned on reference images.
1240
+
1241
+ Args:
1242
+ prompt (`str` or `list[str]`):
1243
+ The prompt(s) to guide generation. For edits, describe the change (e.g.
1244
+ "make the sky purple").
1245
+ image (`PIL.Image.Image`, `np.ndarray`, `torch.Tensor` or a list of them, *optional*):
1246
+ Reference image(s). They are encoded into the prompt conditioning via
1247
+ the Qwen3-VL vision tower and appended to the transformer sequence as
1248
+ clean VAE latents at t=0. References keep their own aspect ratio; the
1249
+ output size is set by `height`/`width` independently.
1250
+ negative_prompt (`str` or `list[str]`, *optional*):
1251
+ Prompt(s) not to guide generation; ignored when `guidance_scale <= 0`.
1252
+ height / width (`int`, defaults to 1024):
1253
+ Output size in pixels; rounded up to a multiple of 16 if needed.
1254
+ num_inference_steps (`int`, *optional*):
1255
+ Denoising steps. Defaults to 8 for a distilled (Turbo) checkpoint and 28
1256
+ otherwise.
1257
+ sigmas (`list[float]`, *optional*):
1258
+ Custom sigma grid for the scheduler.
1259
+ guidance_scale (`float`, *optional*):
1260
+ Krea 2 CFG convention: velocity is `cond + scale * (cond - uncond)` and
1261
+ guidance is enabled whenever `scale > 0` (equals standard CFG with scale
1262
+ `1 + scale`). Defaults to 0.0 for a distilled checkpoint and 4.5
1263
+ otherwise.
1264
+ num_images_per_prompt (`int`, defaults to 1):
1265
+ Number of images per prompt.
1266
+ generator (`torch.Generator` or `list[torch.Generator]`, *optional*):
1267
+ RNG for deterministic generation.
1268
+ latents (`torch.Tensor`, *optional*):
1269
+ Pre-generated packed noisy latents `(B, image_seq_len, in_channels)`.
1270
+ prompt_embeds / prompt_embeds_mask (`torch.Tensor`, *optional*):
1271
+ Pre-computed text conditioning `(B, L, num_text_layers, D)` and its
1272
+ bool mask `(B, L)`; skips prompt encoding when given.
1273
+ negative_prompt_embeds / negative_prompt_embeds_mask (`torch.Tensor`, *optional*):
1274
+ Same, for the negative prompt.
1275
+ reference_max_pixels (`int`, defaults to `1024 * 1024`):
1276
+ Pixel budget each reference image is downscaled to fit before VAE
1277
+ encoding (never upscaled).
1278
+ vl_image_max_pixels (`int`, defaults to `384 * 384`):
1279
+ Pixel budget for the (coarse) Qwen3-VL view of each reference image.
1280
+ encode_reference_in_prompt (`bool`, defaults to `True`):
1281
+ Whether reference images are also embedded into the text conditioning
1282
+ through the Qwen3-VL vision tower (matches AI-Toolkit edit training).
1283
+ kv_cache (`bool`, defaults to `False`):
1284
+ Cache the reference tokens' attention K/V: they are precomputed
1285
+ in a single t=0 pass and reused on every denoising step, so the
1286
+ reference tokens never ride along in the per-step sequence --
1287
+ faster, especially with CFG or many steps. The LoRA must be
1288
+ trained with AI-Toolkit's ``kv_cache`` model kwarg (reference
1289
+ tokens attend only to each other) for this to work properly;
1290
+ leave off for normally trained edit LoRAs.
1291
+ output_type (`str`, defaults to `"pil"`):
1292
+ `"pil"`, `"np"`, `"pt"` or `"latent"`.
1293
+ return_dict (`bool`, defaults to `True`):
1294
+ Whether to return a [`Krea2PipelineOutput`] instead of a plain tuple.
1295
+ attention_kwargs (`dict`, *optional*):
1296
+ Forwarded to the transformer; a `scale` entry sets the LoRA scale.
1297
+ max_sequence_length (`int`, defaults to 512):
1298
+ Maximum prompt token length (truncation only; no fixed padding).
1299
+
1300
+ Returns:
1301
+ [`Krea2PipelineOutput`] or `tuple`: the generated images.
1302
+ """
1303
+ if num_inference_steps is None:
1304
+ num_inference_steps = 8 if self.config.is_distilled else 28
1305
+ if guidance_scale is None:
1306
+ guidance_scale = 0.0 if self.config.is_distilled else 4.5
1307
+
1308
+ multiple = self.vae_scale_factor * self.patch_size
1309
+ if height % multiple != 0 or width % multiple != 0:
1310
+ rounded_height = ((height + multiple - 1) // multiple) * multiple
1311
+ rounded_width = ((width + multiple - 1) // multiple) * multiple
1312
+ logger.warning(
1313
+ f"`height` and `width` must be multiples of {multiple}; rounding up from {height}x{width} to"
1314
+ f" {rounded_height}x{rounded_width}."
1315
+ )
1316
+ height, width = rounded_height, rounded_width
1317
+
1318
+ if prompt is None and prompt_embeds is None:
1319
+ raise ValueError("Provide either `prompt` or `prompt_embeds`.")
1320
+ if prompt_embeds is not None and prompt_embeds_mask is None:
1321
+ raise ValueError("`prompt_embeds` requires `prompt_embeds_mask`.")
1322
+ if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
1323
+ raise ValueError("`negative_prompt_embeds` requires `negative_prompt_embeds_mask`.")
1324
+
1325
+ self._guidance_scale = guidance_scale
1326
+
1327
+ if prompt is not None and isinstance(prompt, str):
1328
+ batch_size = 1
1329
+ elif prompt is not None:
1330
+ batch_size = len(prompt)
1331
+ else:
1332
+ batch_size = prompt_embeds.shape[0]
1333
+
1334
+ device = self._execution_device
1335
+ transformer_dtype = self.transformer.dtype
1336
+
1337
+ # 1. Normalize reference images to a list of [0, 1] CHW tensors.
1338
+ ref_images = None
1339
+ if image is not None:
1340
+ image_list = image if isinstance(image, (list, tuple)) else [image]
1341
+ ref_images = [self._to_chw_tensor(img) for img in image_list]
1342
+
1343
+ # 2. Encode the prompt(s). With references, the coarse VL view of each image is
1344
+ # embedded in the user message so the text conditioning "sees" them.
1345
+ vl_images = None
1346
+ if ref_images is not None and encode_reference_in_prompt:
1347
+ vl_images = self._prep_vl_images([img.to(device) for img in ref_images], vl_image_max_pixels)
1348
+
1349
+ if prompt_embeds is None:
1350
+ prompt_embeds, prompt_embeds_mask = self.encode_prompt(
1351
+ prompt, vl_images, num_images_per_prompt, max_sequence_length, device
1352
+ )
1353
+ prompt_embeds = prompt_embeds.to(transformer_dtype)
1354
+
1355
+ if self.do_classifier_free_guidance:
1356
+ if negative_prompt_embeds is None:
1357
+ negative_prompt = negative_prompt if negative_prompt is not None else ""
1358
+ if isinstance(negative_prompt, str):
1359
+ negative_prompt = [negative_prompt] * batch_size
1360
+ negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
1361
+ negative_prompt, vl_images, num_images_per_prompt, max_sequence_length, device
1362
+ )
1363
+ negative_prompt_embeds = negative_prompt_embeds.to(transformer_dtype)
1364
+
1365
+ # 3. Prepare the noisy latents (kept in float32 across scheduler steps).
1366
+ num_channels_latents = self.transformer.config.in_channels // (self.patch_size**2)
1367
+ latents = self.prepare_latents(
1368
+ batch_size * num_images_per_prompt,
1369
+ num_channels_latents,
1370
+ height,
1371
+ width,
1372
+ torch.float32,
1373
+ device,
1374
+ generator,
1375
+ latents,
1376
+ )
1377
+ grid_height = height // (self.vae_scale_factor * self.patch_size)
1378
+ grid_width = width // (self.vae_scale_factor * self.patch_size)
1379
+
1380
+ # 4. Encode + pack reference latents (shared across the batch) and build the
1381
+ # combined rotary coordinates.
1382
+ ref_tokens, ref_seq_len = None, 0
1383
+ neg_position_ids = None
1384
+ position_ids = self.prepare_position_ids(prompt_embeds.shape[1], grid_height, grid_width, device)
1385
+ if self.do_classifier_free_guidance:
1386
+ neg_position_ids = self.prepare_position_ids(
1387
+ negative_prompt_embeds.shape[1], grid_height, grid_width, device
1388
+ )
1389
+ ref_kv = None
1390
+ if ref_images is not None:
1391
+ ref_latents = self._encode_reference_latents(ref_images, reference_max_pixels, generator, device)
1392
+ ref_tokens, ref_position_ids = self._pack_reference_latents(ref_latents, device, transformer_dtype)
1393
+ ref_seq_len = ref_tokens.shape[1]
1394
+ ref_tokens = ref_tokens.expand(latents.shape[0], -1, -1)
1395
+ if kv_cache:
1396
+ # Precompute pass: the refs alone run through the blocks once at t=0
1397
+ # and every denoising step reuses their per-block K/V, so the ref
1398
+ # tokens are dropped from the per-step sequence entirely.
1399
+ ref_kv = self.transformer.precompute_ref_kv(ref_tokens, ref_position_ids, attention_kwargs)
1400
+ ref_tokens, ref_seq_len = None, 0
1401
+ else:
1402
+ position_ids = torch.cat([position_ids, ref_position_ids], dim=0)
1403
+ if neg_position_ids is not None:
1404
+ neg_position_ids = torch.cat([neg_position_ids, ref_position_ids], dim=0)
1405
+
1406
+ # 5. Prepare timesteps. The distilled (Turbo) checkpoint was trained at a fixed
1407
+ # exponential time shift mu=1.15; the base checkpoint interpolates mu from the
1408
+ # image token count.
1409
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
1410
+ if self.config.is_distilled:
1411
+ mu = 1.15
1412
+ else:
1413
+ mu = calculate_shift(
1414
+ grid_height * grid_width,
1415
+ self.scheduler.config.get("base_image_seq_len", 256),
1416
+ self.scheduler.config.get("max_image_seq_len", 6400),
1417
+ self.scheduler.config.get("base_shift", 0.5),
1418
+ self.scheduler.config.get("max_shift", 1.15),
1419
+ )
1420
+ self.scheduler.set_timesteps(sigmas=sigmas, device=device, mu=mu)
1421
+ timesteps = self.scheduler.timesteps
1422
+ self.scheduler.set_begin_index(0)
1423
+
1424
+ # 6. Denoising loop (Euler flow ODE integration via the scheduler).
1425
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
1426
+ for t in timesteps:
1427
+ timestep = (t / self.scheduler.config.num_train_timesteps).expand(latents.shape[0]).to(
1428
+ transformer_dtype
1429
+ )
1430
+
1431
+ model_input = latents.to(transformer_dtype)
1432
+ if ref_tokens is not None:
1433
+ model_input = torch.cat([model_input, ref_tokens], dim=1)
1434
+
1435
+ noise_pred = self.transformer(
1436
+ hidden_states=model_input,
1437
+ encoder_hidden_states=prompt_embeds,
1438
+ timestep=timestep,
1439
+ position_ids=position_ids,
1440
+ encoder_attention_mask=prompt_embeds_mask,
1441
+ ref_seq_len=ref_seq_len,
1442
+ ref_kv_cache=ref_kv,
1443
+ attention_kwargs=attention_kwargs,
1444
+ return_dict=False,
1445
+ )[0]
1446
+
1447
+ if self.do_classifier_free_guidance:
1448
+ neg_noise_pred = self.transformer(
1449
+ hidden_states=model_input,
1450
+ encoder_hidden_states=negative_prompt_embeds,
1451
+ timestep=timestep,
1452
+ position_ids=neg_position_ids,
1453
+ encoder_attention_mask=negative_prompt_embeds_mask,
1454
+ ref_seq_len=ref_seq_len,
1455
+ ref_kv_cache=ref_kv,
1456
+ attention_kwargs=attention_kwargs,
1457
+ return_dict=False,
1458
+ )[0]
1459
+ noise_pred = noise_pred + guidance_scale * (noise_pred - neg_noise_pred)
1460
+
1461
+ latents = self.scheduler.step(noise_pred.float(), t, latents, return_dict=False)[0]
1462
+ progress_bar.update()
1463
+
1464
+ # 7. Decode latents.
1465
+ if output_type == "latent":
1466
+ image_out = latents
1467
+ else:
1468
+ latents = self._unpack_latents(latents, height, width).to(self.vae.dtype)
1469
+ latents_mean = (
1470
+ torch.tensor(self.vae.config.latents_mean)
1471
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
1472
+ .to(latents.device, latents.dtype)
1473
+ )
1474
+ latents_std = (
1475
+ torch.tensor(self.vae.config.latents_std)
1476
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
1477
+ .to(latents.device, latents.dtype)
1478
+ )
1479
+ latents = latents * latents_std + latents_mean
1480
+ image_out = self.vae.decode(latents, return_dict=False)[0][:, :, 0]
1481
+ image_out = self.image_processor.postprocess(image_out, output_type=output_type)
1482
+
1483
+ self.maybe_free_model_hooks()
1484
+
1485
+ if not return_dict:
1486
+ return (image_out,)
1487
+ return Krea2PipelineOutput(images=image_out)