Lakonik commited on
Commit
8a62807
·
1 Parent(s): 37b9023

Add AsymFLUX.2-klein Space demo

Browse files
.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /.idea/
2
+ /work_dirs*
3
+ .vscode/
4
+ /tmp
5
+ /data
6
+ /checkpoints
7
+ *.so
8
+ *.patch
9
+ __pycache__/
10
+ *.egg-info/
11
+ /viz*
12
+ /submit*
13
+ build/
14
+ *.pyd
15
+ /cache*
16
+ *.stl
17
+ *.pth
18
+ /venv/
19
+ .nk8s
20
+ *.mp4
21
+ .vs
22
+ /exp/
23
+ /dev/
24
+ *.pyi
25
+ !/data/imagenet/imagenet1000_clsidx_to_labels.txt
LICENSE.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # License for AsymFLUX.2-klein
2
+
3
+ This repository distributes an **AsymFLUX.2-klein app** that is a **Derivative** of
4
+ **FLUX.2 klein Base 9B** by **Black Forest Labs Inc.**
5
+
6
+ Use and distribution of this app are governed by the **FLUX Non-Commercial License**.
7
+ No commercial use of this app or its derivatives is permitted without a separate
8
+ commercial license from Black Forest Labs.
9
+
10
+ - Full license: https://huggingface.co/black-forest-labs/FLUX.2-klein-base-9B/blob/main/LICENSE.md
11
+ - This repository does not grant any rights beyond the license above.
README.md CHANGED
@@ -1,14 +1,26 @@
1
  ---
2
- title: AsymFLUX.2 Klein
3
- emoji: 📈
4
- colorFrom: green
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
- short_description: High-fidelity pixel-space text-to-image generation
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: AsymFLUX.2-klein Demo
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 5.49.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: other
11
+ license_name: flux-non-commercial-license
12
+ license_link: LICENSE.md
13
  ---
14
 
15
+ Official demo of the paper:
16
+
17
+ **Asymmetric Flow Models**
18
+ <br>
19
+ [Hansheng Chen](https://lakonik.github.io/),
20
+ Jan Ackermann,
21
+ Minseo Kim,
22
+ [Gordon Wetzstein](http://web.stanford.edu/~gordonwz/),
23
+ [Leonidas Guibas](https://geometry.stanford.edu/?member=guibas)<br>
24
+ Stanford University
25
+ <br>
26
+ [[Paper](https://hanshengchen.com/asymflow/static/paper.pdf)] [[Project](https://hanshengchen.com/asymflow)] [[Code](https://github.com/Lakonik/piFlow)]
app.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import os
3
+ import random
4
+
5
+ import gradio as gr
6
+ import numpy as np
7
+ import spaces
8
+ import torch
9
+
10
+ from lakonlab.models.architectures import OklabColorEncoder
11
+ from lakonlab.models.diffusions.schedulers import FlowAdapterScheduler
12
+ from lakonlab.pipelines.pipeline_pixelflux2_klein import PixelFlux2KleinPipeline
13
+ from lakonlab.pipelines.prompt_rewriters.qwen3_vl import Qwen3VLPromptRewriter
14
+ from lakonlab.ui.gradio.create_text_to_img import create_interface_text_to_img
15
+
16
+
17
+ torch.backends.cuda.matmul.allow_tf32 = True
18
+ torch.backends.cudnn.allow_tf32 = True
19
+
20
+
21
+ DEFAULT_PROMPT = (
22
+ 'Restored color photo from the 1900s. A middle-aged man with cybernetic metal hands is sitting on an old wooden '
23
+ 'chair and reading the newspaper. The newspaper has the prominent headline "AsymFLOW RELEASED" in large bold font. '
24
+ 'Close-up shot focusing on the newspaper.'
25
+ )
26
+ DEFAULT_NEG_PROMPT = 'Low quality, worst quality, blurry, deformed, bad anatomy, unclear text'
27
+
28
+ SYSTEM_PROMPT_TEXT_ONLY_PATH = 'lakonlab/pipelines/prompt_rewriters/system_prompts/default_text_only.txt'
29
+
30
+
31
+ def set_random_seed(seed: int, deterministic: bool = True) -> None:
32
+ random.seed(seed)
33
+ np.random.seed(seed)
34
+ torch.manual_seed(seed)
35
+ torch.cuda.manual_seed(seed)
36
+ torch.cuda.manual_seed_all(seed)
37
+ os.environ['PYTHONHASHSEED'] = str(seed)
38
+ if deterministic:
39
+ torch.backends.cudnn.deterministic = True
40
+ torch.backends.cudnn.benchmark = False
41
+
42
+
43
+ pipe = PixelFlux2KleinPipeline.from_pretrained(
44
+ 'black-forest-labs/FLUX.2-klein-base-9B',
45
+ vae=OklabColorEncoder(
46
+ use_affine_norm=True,
47
+ mean=(0.56, 0.0, 0.01),
48
+ std=0.16),
49
+ scheduler=FlowAdapterScheduler(
50
+ shift=17.0,
51
+ use_dynamic_shifting=True,
52
+ base_seq_len=1024 ** 2,
53
+ max_seq_len=2048 ** 2,
54
+ base_logshift=math.log(17.0),
55
+ max_logshift=math.log(34.0),
56
+ dynamic_shifting_type='sqrt',
57
+ base_scheduler='UniPCMultistep'),
58
+ torch_dtype=torch.bfloat16)
59
+ pipe.load_lakonlab_adapter(
60
+ 'Lakonik/AsymFLUX.2-klein-9B',
61
+ target_module_name='transformer')
62
+ pipe = pipe.to('cuda')
63
+
64
+ prompt_rewriter = Qwen3VLPromptRewriter(
65
+ device_map='cuda',
66
+ system_prompt_text_only=open(SYSTEM_PROMPT_TEXT_ONLY_PATH, 'r').read(),
67
+ max_new_tokens_default=512,
68
+ )
69
+
70
+
71
+ @spaces.GPU
72
+ def run_rewrite_prompt_gpu(seed, prompt, progress):
73
+ set_random_seed(seed)
74
+ progress(0.05, desc='Rewriting prompt...')
75
+ return prompt_rewriter.rewrite_text_batch([prompt])[0]
76
+
77
+
78
+ def run_rewrite_prompt(seed, prompt, rewrite_prompt, progress=gr.Progress(track_tqdm=True)):
79
+ if rewrite_prompt:
80
+ return run_rewrite_prompt_gpu(seed, prompt, progress), None
81
+ return '', None
82
+
83
+
84
+ @spaces.GPU
85
+ def generate(
86
+ seed, prompt, negative_prompt, rewrite_prompt, rewritten_prompt, width, height, steps, guidance_scale,
87
+ progress=gr.Progress(track_tqdm=True)):
88
+ return pipe(
89
+ prompt=rewritten_prompt if rewrite_prompt else prompt,
90
+ negative_prompt=negative_prompt,
91
+ width=width,
92
+ height=height,
93
+ num_inference_steps=steps,
94
+ guidance_scale=guidance_scale,
95
+ generator=torch.Generator().manual_seed(seed),
96
+ ).images[0]
97
+
98
+
99
+ with gr.Blocks(
100
+ analytics_enabled=False,
101
+ title='AsymFLUX.2-klein Demo',
102
+ css_paths='lakonlab/ui/gradio/style.css') as demo:
103
+ gr.Markdown(
104
+ '# AsymFLUX.2-klein Demo\n\n'
105
+ 'Pixel-space text-to-image generation demo of the paper '
106
+ '[Asymmetric Flow Models](https://hanshengchen.com/asymflow/static/paper.pdf). '
107
+ '**Base model:** [FLUX.2 klein Base 9B](https://huggingface.co/black-forest-labs/FLUX.2-klein-base-9B). '
108
+ '**Code:** [https://github.com/Lakonik/piFlow](https://github.com/Lakonik/piFlow).\n'
109
+ '<br> Use and distribution of this app are governed by the '
110
+ '[FLUX Non-Commercial License](https://huggingface.co/black-forest-labs/FLUX.2-klein-base-9B/blob/main/LICENSE.md).'
111
+ )
112
+
113
+ create_interface_text_to_img(
114
+ generate,
115
+ prompt=DEFAULT_PROMPT,
116
+ negative_prompt=DEFAULT_NEG_PROMPT,
117
+ steps=38,
118
+ min_steps=4,
119
+ max_steps=50,
120
+ guidance_scale=4.0,
121
+ height=1280,
122
+ width=960,
123
+ create_negative_prompt=True,
124
+ create_prompt_rewrite=True,
125
+ args=[
126
+ 'last_seed',
127
+ 'prompt',
128
+ 'negative_prompt',
129
+ 'rewrite_prompt',
130
+ 'rewritten_prompt',
131
+ 'width',
132
+ 'height',
133
+ 'steps',
134
+ 'guidance_scale',
135
+ ],
136
+ rewrite_prompt_api=run_rewrite_prompt,
137
+ rewrite_prompt_args=['last_seed', 'prompt', 'rewrite_prompt'])
138
+
139
+ demo.queue().launch()
lakonlab/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = '0.2.0'
lakonlab/models/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
lakonlab/models/architectures/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .autoencoders import OklabColorEncoder, RGBColorEncoder
2
+
3
+ __all__ = ['OklabColorEncoder', 'RGBColorEncoder']
lakonlab/models/architectures/asymflow/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .asymflux2 import _AsymFlux2Transformer2DModel
2
+
3
+ __all__ = ['_AsymFlux2Transformer2DModel']
lakonlab/models/architectures/asymflow/asymflux2.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Optional, List
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+
6
+ from diffusers.models.transformers.transformer_flux2 import (
7
+ Flux2Transformer2DModel, Flux2PosEmbed, Flux2TransformerBlock, Flux2SingleTransformerBlock,
8
+ Flux2TimestepGuidanceEmbeddings, Flux2Modulation)
9
+ from diffusers.models.normalization import AdaLayerNormContinuous
10
+ from diffusers.configuration_utils import register_to_config
11
+ from diffusers.utils import apply_lora_scale
12
+
13
+ from .common import AsymFlowMixin
14
+
15
+
16
+ class _AsymFlux2Transformer2DModel(AsymFlowMixin, Flux2Transformer2DModel):
17
+
18
+ @register_to_config
19
+ def __init__(
20
+ self,
21
+ patch_size=16,
22
+ in_channels: int = 3,
23
+ base_rank: int = 128,
24
+ num_layers: int = 8,
25
+ num_single_layers: int = 48,
26
+ attention_head_dim: int = 128,
27
+ num_attention_heads: int = 48,
28
+ joint_attention_dim: int = 15360,
29
+ timestep_guidance_channels: int = 256,
30
+ mlp_ratio: float = 3.0,
31
+ axes_dims_rope: tuple[int, ...] = (32, 32, 32, 32),
32
+ rope_theta: int = 2000,
33
+ eps: float = 1e-6,
34
+ sigma_min: float = 1e-4,
35
+ num_timesteps=1,
36
+ guidance_embeds: bool = True):
37
+ super(Flux2Transformer2DModel, self).__init__()
38
+
39
+ self.patch_size = patch_size
40
+
41
+ self.in_channels = in_channels
42
+ self.out_channels = in_channels
43
+
44
+ self.inner_dim = num_attention_heads * attention_head_dim
45
+
46
+ # 1. Sinusoidal positional embedding for RoPE on image and text tokens
47
+ self.pos_embed = Flux2PosEmbed(theta=rope_theta, axes_dim=axes_dims_rope)
48
+
49
+ # 2. Combined timestep + guidance embedding
50
+ self.time_guidance_embed = Flux2TimestepGuidanceEmbeddings(
51
+ in_channels=timestep_guidance_channels,
52
+ embedding_dim=self.inner_dim,
53
+ bias=False,
54
+ guidance_embeds=guidance_embeds,
55
+ )
56
+
57
+ # 3. Modulation (double stream and single stream blocks share modulation parameters, resp.)
58
+ # Two sets of shift/scale/gate modulation parameters for the double stream attn and FF sub-blocks
59
+ self.double_stream_modulation_img = Flux2Modulation(self.inner_dim, mod_param_sets=2, bias=False)
60
+ self.double_stream_modulation_txt = Flux2Modulation(self.inner_dim, mod_param_sets=2, bias=False)
61
+ # Only one set of modulation parameters as the attn and FF sub-blocks are run in parallel for single stream
62
+ self.single_stream_modulation = Flux2Modulation(self.inner_dim, mod_param_sets=1, bias=False)
63
+
64
+ # 4. Input projections
65
+ self.x_embedder = nn.Linear(in_channels * (patch_size ** 2), self.inner_dim, bias=False)
66
+ self.context_embedder = nn.Linear(joint_attention_dim, self.inner_dim, bias=False)
67
+
68
+ # 5. Double Stream Transformer Blocks
69
+ self.transformer_blocks = nn.ModuleList(
70
+ [
71
+ Flux2TransformerBlock(
72
+ dim=self.inner_dim,
73
+ num_attention_heads=num_attention_heads,
74
+ attention_head_dim=attention_head_dim,
75
+ mlp_ratio=mlp_ratio,
76
+ eps=eps,
77
+ bias=False,
78
+ )
79
+ for _ in range(num_layers)
80
+ ]
81
+ )
82
+
83
+ # 6. Single Stream Transformer Blocks
84
+ self.single_transformer_blocks = nn.ModuleList(
85
+ [
86
+ Flux2SingleTransformerBlock(
87
+ dim=self.inner_dim,
88
+ num_attention_heads=num_attention_heads,
89
+ attention_head_dim=attention_head_dim,
90
+ mlp_ratio=mlp_ratio,
91
+ eps=eps,
92
+ bias=False,
93
+ )
94
+ for _ in range(num_single_layers)
95
+ ]
96
+ )
97
+
98
+ # 7. Output layers
99
+ self.norm_out = AdaLayerNormContinuous(
100
+ self.inner_dim, self.inner_dim, elementwise_affine=False, eps=eps, bias=False
101
+ )
102
+ self.proj_out = nn.Linear(
103
+ self.inner_dim, self.out_channels * (patch_size ** 2), bias=False
104
+ )
105
+
106
+ # 8. AsymFlow attributes and buffers
107
+ self.base_rank = base_rank
108
+ self.sigma_min = sigma_min
109
+ self.num_timesteps = num_timesteps
110
+ self.init_asymflow_buffers(self.in_channels * (patch_size ** 2), self.base_rank)
111
+
112
+ self.gradient_checkpointing = False
113
+
114
+ @staticmethod
115
+ def patchify(latents, patch_size, pack_channels=True):
116
+ bs, c, h, w = latents.size()
117
+ latents = latents.reshape(
118
+ bs, c, h // patch_size, patch_size, w // patch_size, patch_size
119
+ ).permute(
120
+ 0, 1, 3, 5, 2, 4
121
+ )
122
+ if pack_channels:
123
+ latents = latents.reshape(
124
+ bs, c * patch_size * patch_size, h // patch_size, w // patch_size)
125
+ else:
126
+ latents = latents.reshape(
127
+ bs, c, patch_size * patch_size, h // patch_size, w // patch_size)
128
+ return latents
129
+
130
+ @staticmethod
131
+ def unpatchify(latents, patch_size, packed_channels=True):
132
+ if packed_channels:
133
+ bs, c, h, w = latents.size()
134
+ latents = latents.reshape(
135
+ bs, c // (patch_size * patch_size), patch_size, patch_size, h, w
136
+ ).permute(
137
+ 0, 1, 4, 2, 5, 3
138
+ ).reshape(
139
+ bs, c // (patch_size * patch_size), h * patch_size, w * patch_size)
140
+ else:
141
+ bs, c, _, h, w = latents.size()
142
+ latents = latents.reshape(
143
+ bs, c, patch_size, patch_size, h, w
144
+ ).permute(
145
+ 0, 1, 4, 2, 5, 3
146
+ ).reshape(
147
+ bs, c, h * patch_size, w * patch_size)
148
+ return latents
149
+
150
+ @staticmethod
151
+ def pack(latents):
152
+ bs, c, h, w = latents.shape
153
+ latents = latents.reshape(bs, c, h * w).permute(0, 2, 1)
154
+ return latents
155
+
156
+ @staticmethod
157
+ def unpack(latents, h, w):
158
+ bs, _, c = latents.shape
159
+ latents = latents.permute(0, 2, 1).reshape(bs, c, h, w)
160
+ return latents
161
+
162
+ @staticmethod
163
+ def _prepare_latent_ids(latents):
164
+ batch_size, _, height, width = latents.shape
165
+
166
+ t = torch.arange(1)
167
+ h = torch.arange(height)
168
+ w = torch.arange(width)
169
+ l = torch.arange(1)
170
+
171
+ latent_ids = torch.cartesian_prod(t, h, w, l)
172
+ latent_ids = latent_ids.unsqueeze(0).expand(batch_size, -1, -1)
173
+
174
+ return latent_ids.to(device=latents.device)
175
+
176
+ @staticmethod
177
+ def _prepare_condition_latent_ids(
178
+ image_latents: List[torch.Tensor],
179
+ scale: int = 10):
180
+ if not isinstance(image_latents, list):
181
+ raise ValueError(f"Expected `image_latents` to be a list, got {type(image_latents)}.")
182
+
183
+ t_coords = [scale + scale * t for t in torch.arange(0, len(image_latents))]
184
+ t_coords = [t.view(-1) for t in t_coords]
185
+
186
+ image_latent_ids = []
187
+ for x, t in zip(image_latents, t_coords):
188
+ _, _, h, w = x.shape
189
+ x_ids = torch.cartesian_prod(t, torch.arange(h), torch.arange(w), torch.arange(1))
190
+ image_latent_ids.append(x_ids)
191
+
192
+ image_latent_ids = torch.cat(image_latent_ids, dim=0)
193
+ image_latent_ids = image_latent_ids.unsqueeze(0).expand(image_latents[0].size(0), -1, -1)
194
+
195
+ return image_latent_ids.to(device=image_latents[0].device)
196
+
197
+ def _get_rotary_emb(self, img_ids, txt_ids):
198
+ if img_ids.ndim == 3:
199
+ img_ids = img_ids[0]
200
+ if txt_ids.ndim == 3:
201
+ txt_ids = txt_ids[0]
202
+
203
+ with torch.autocast(device_type='cuda', dtype=torch.float32, enabled=False):
204
+ img_ids = img_ids.float()
205
+ image_rotary_emb = self.pos_embed(img_ids)
206
+ text_rotary_emb = self.pos_embed(txt_ids)
207
+ return (
208
+ torch.cat([text_rotary_emb[0], image_rotary_emb[0]], dim=0),
209
+ torch.cat([text_rotary_emb[1], image_rotary_emb[1]], dim=0),
210
+ )
211
+
212
+ @apply_lora_scale("joint_attention_kwargs")
213
+ def forward(
214
+ self,
215
+ x_t: torch.Tensor,
216
+ timestep: torch.Tensor,
217
+ encoder_hidden_states: torch.Tensor = None,
218
+ condition_latents: List[torch.Tensor] | None = None,
219
+ txt_ids: torch.Tensor = None,
220
+ guidance: torch.Tensor = None,
221
+ joint_attention_kwargs: dict[str, Any] | None = None):
222
+ x_t = self.patchify(x_t, self.patch_size)
223
+ img_ids = self._prepare_latent_ids(x_t)
224
+
225
+ bs, _, h, w = x_t.size()
226
+ x_t_packed = self.pack(x_t)
227
+ num_x_tokens = x_t_packed.size(1)
228
+ packed_ndim = x_t_packed.dim()
229
+
230
+ # scale and timestep calibration
231
+ calibration = self.asymflow_calibration(timestep, bs, packed_ndim)
232
+ hidden_states = x_t_packed * calibration.k.to(x_t_packed.dtype)
233
+
234
+ input_img_ids = img_ids
235
+ if condition_latents is not None:
236
+ condition_hidden_states = [self.patchify(z, self.patch_size) for z in condition_latents]
237
+ condition_latent_ids = self._prepare_condition_latent_ids(condition_hidden_states)
238
+ condition_hidden_states = [self.pack(z) / calibration.s for z in condition_hidden_states]
239
+ hidden_states = torch.cat([hidden_states] + condition_hidden_states, dim=1)
240
+ input_img_ids = torch.cat([img_ids, condition_latent_ids], dim=1)
241
+
242
+ hidden_states = self.x_embedder(hidden_states)
243
+
244
+ num_txt_tokens = encoder_hidden_states.shape[1]
245
+
246
+ if guidance is not None:
247
+ guidance = guidance.to(hidden_states.dtype) * 1000
248
+ temb = self.time_guidance_embed(calibration.timestep.to(hidden_states.dtype) * 1000, guidance)
249
+
250
+ double_stream_mod_img = self.double_stream_modulation_img(temb)
251
+ double_stream_mod_txt = self.double_stream_modulation_txt(temb)
252
+ single_stream_mod = self.single_stream_modulation(temb)
253
+
254
+ encoder_hidden_states = self.context_embedder(encoder_hidden_states)
255
+ concat_rotary_emb = self._get_rotary_emb(input_img_ids, txt_ids)
256
+
257
+ for block in self.transformer_blocks:
258
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
259
+ encoder_hidden_states, hidden_states = self._gradient_checkpointing_func(
260
+ block,
261
+ hidden_states,
262
+ encoder_hidden_states,
263
+ double_stream_mod_img,
264
+ double_stream_mod_txt,
265
+ concat_rotary_emb,
266
+ joint_attention_kwargs,
267
+ )
268
+ else:
269
+ encoder_hidden_states, hidden_states = block(
270
+ hidden_states=hidden_states,
271
+ encoder_hidden_states=encoder_hidden_states,
272
+ temb_mod_img=double_stream_mod_img,
273
+ temb_mod_txt=double_stream_mod_txt,
274
+ image_rotary_emb=concat_rotary_emb,
275
+ joint_attention_kwargs=joint_attention_kwargs,
276
+ )
277
+
278
+ hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1)
279
+
280
+ for block in self.single_transformer_blocks:
281
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
282
+ hidden_states = self._gradient_checkpointing_func(
283
+ block,
284
+ hidden_states,
285
+ None,
286
+ single_stream_mod,
287
+ concat_rotary_emb,
288
+ joint_attention_kwargs,
289
+ )
290
+ else:
291
+ hidden_states = block(
292
+ hidden_states=hidden_states,
293
+ encoder_hidden_states=None,
294
+ temb_mod=single_stream_mod,
295
+ image_rotary_emb=concat_rotary_emb,
296
+ joint_attention_kwargs=joint_attention_kwargs,
297
+ )
298
+
299
+ hidden_states = hidden_states[:, num_txt_tokens:num_txt_tokens + num_x_tokens]
300
+ hidden_states = self.norm_out(hidden_states, temb)
301
+
302
+ u_a_packed = self.proj_out(hidden_states)
303
+
304
+ output_packed = self.asymflow_velocity(u_a_packed, x_t_packed, calibration)
305
+
306
+ output = self.unpack(output_packed.to(hidden_states.dtype), h, w)
307
+ output = self.unpatchify(output, self.patch_size)
308
+
309
+ return output
lakonlab/models/architectures/asymflow/common.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import NamedTuple
2
+
3
+ import torch
4
+ import torch.nn.functional as F
5
+
6
+
7
+ class AsymFlowCalibration(NamedTuple):
8
+ s: torch.Tensor
9
+ k: torch.Tensor
10
+ timestep: torch.Tensor
11
+ sigma: torch.Tensor
12
+
13
+
14
+ class AsymFlowMixin:
15
+
16
+ train_sigma_min = 1e-6
17
+
18
+ def init_asymflow_buffers(self, patch_dim: int, base_rank: int):
19
+ assert patch_dim >= base_rank
20
+ eye = torch.eye(base_rank)
21
+ self.register_buffer('proj_buffer', F.pad(eye, (0, 0, 0, patch_dim - base_rank))) # (patch_dim, base_rank)
22
+ self.register_buffer('scale_buffer', torch.tensor(1.0))
23
+
24
+ def asymflow_calibration(self, timestep, batch_size: int, ndim: int):
25
+ with torch.autocast(device_type='cuda', dtype=torch.float32, enabled=False):
26
+ timestep = timestep.float()
27
+ s = self.scale_buffer.float()
28
+ sigma = timestep / self.num_timesteps
29
+ k = 1 / (s + (1 - s) * sigma)
30
+ cal_timestep = timestep * k
31
+ sigma = sigma.expand(batch_size).reshape(batch_size, *((ndim - 1) * [1])).float()
32
+ k = k.reshape(batch_size, *((ndim - 1) * [1]))
33
+ return AsymFlowCalibration(
34
+ s=s,
35
+ k=k,
36
+ timestep=cal_timestep,
37
+ sigma=sigma,
38
+ )
39
+
40
+ @staticmethod
41
+ def orthogonal_decomposition(full_rank_state, proj_buffer):
42
+ subspace = full_rank_state @ proj_buffer @ proj_buffer.T
43
+ complement = full_rank_state - subspace
44
+ return subspace, complement
45
+
46
+ def asymflow_velocity(
47
+ self,
48
+ u_a_packed,
49
+ x_t_packed,
50
+ calibration: AsymFlowCalibration):
51
+ with torch.autocast(device_type='cuda', dtype=torch.float32, enabled=False):
52
+ sigma_min = self.train_sigma_min if self.training else self.sigma_min
53
+ u_a_packed = u_a_packed.float()
54
+ x_t_packed = x_t_packed.float()
55
+ proj_buffer = self.proj_buffer.float()
56
+ # orthogonal decomposition
57
+ u_a_subspace, u_a_complement = self.orthogonal_decomposition(u_a_packed, proj_buffer)
58
+ x_t_subspace, x_t_complement = self.orthogonal_decomposition(x_t_packed, proj_buffer)
59
+ # read calibration output
60
+ sk = calibration.s * calibration.k
61
+ sigma_clamped = calibration.sigma.clamp(min=sigma_min)
62
+ # low-rank subspace
63
+ u_subspace = (
64
+ sk * u_a_subspace
65
+ + (1 - sk) / sigma_clamped * x_t_subspace
66
+ )
67
+ # orthogonal complement
68
+ u_complement = (x_t_complement + calibration.s * u_a_complement) / sigma_clamped
69
+ # full velocity
70
+ return u_subspace + u_complement
lakonlab/models/architectures/autoencoders/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .color_encoders import OklabColorEncoder, RGBColorEncoder
2
+
3
+ __all__ = ['OklabColorEncoder', 'RGBColorEncoder']
lakonlab/models/architectures/autoencoders/color_encoders.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from diffusers.models.modeling_utils import ModelMixin
4
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
5
+
6
+
7
+ class OklabColorEncoder(ModelMixin, ConfigMixin):
8
+
9
+ @register_to_config
10
+ def __init__(
11
+ self,
12
+ use_affine_norm=True,
13
+ mean=(0.5, 0.0, 0.0),
14
+ std=0.21,
15
+ ):
16
+ super().__init__()
17
+ self.use_affine_norm = use_affine_norm
18
+ self.register_buffer('lrgb_to_lms', torch.tensor([
19
+ [0.4122214708, 0.5363325363, 0.0514459929],
20
+ [0.2119034982, 0.6806995451, 0.1073969566],
21
+ [0.0883024619, 0.2817188376, 0.6299787005]
22
+ ], dtype=torch.float32))
23
+ self.register_buffer('lms_to_oklab', torch.tensor([
24
+ [0.2104542553, 0.7936177850, -0.0040720468],
25
+ [1.9779984951, -2.4285922050, 0.4505937099],
26
+ [0.0259040371, 0.7827717662, -0.8086757660]
27
+ ], dtype=torch.float32))
28
+ self.register_buffer('oklab_to_lms', torch.linalg.inv(self.lms_to_oklab))
29
+ self.register_buffer('lms_to_lrgb', torch.linalg.inv(self.lrgb_to_lms))
30
+ if self.use_affine_norm:
31
+ self.register_buffer('affine_mean', torch.tensor(mean, dtype=torch.float32))
32
+ self.register_buffer('affine_std', torch.tensor(std, dtype=torch.float32))
33
+
34
+ @property
35
+ def dtype(self):
36
+ return self.lrgb_to_lms.dtype
37
+
38
+ @staticmethod
39
+ def srgb_to_lrgb(srgb):
40
+ a = 0.055
41
+ return torch.where(srgb <= 0.04045, srgb / 12.92, ((srgb + a) / (1 + a)) ** 2.4)
42
+
43
+ @staticmethod
44
+ def lrgb_to_srgb(lrgb):
45
+ lrgb = lrgb.clamp(min=0)
46
+ a = 0.055
47
+ return torch.where(lrgb <= 0.0031308, lrgb * 12.92, (1 + a) * (lrgb ** (1 / 2.4)) - a)
48
+
49
+ def lrgb_to_oklab(self, lrgb):
50
+ """
51
+ Args:
52
+ lrgb (torch.Tensor): Linear RGB, shape (N, 3, *)
53
+ """
54
+ lms = torch.einsum('ij,bj...->bi...', self.lrgb_to_lms, lrgb).clamp(min=0)
55
+ oklab = torch.einsum('ij,bj...->bi...', self.lms_to_oklab, lms.pow(1/3))
56
+ return oklab
57
+
58
+ def oklab_to_lrgb(self, oklab):
59
+ """
60
+ Args:
61
+ oklab (torch.Tensor): Oklab, shape (N, 3, *)
62
+ """
63
+ lms = torch.einsum('ij,bj...->bi...', self.oklab_to_lms, oklab).pow(3)
64
+ lrgb = torch.einsum('ij,bj...->bi...', self.lms_to_lrgb, lms)
65
+ return lrgb.clamp(0, 1)
66
+
67
+ def encode(self, img):
68
+ rgb = img / 2 + 0.5
69
+ lrgb = self.srgb_to_lrgb(rgb)
70
+ oklab = self.lrgb_to_oklab(lrgb)
71
+ if self.use_affine_norm:
72
+ n_dim = img.dim() - 2
73
+ mean = self.affine_mean.reshape(-1, *([1] * n_dim))
74
+ std = self.affine_std.reshape(-1, *([1] * n_dim))
75
+ oklab = (oklab - mean) / std
76
+ return oklab
77
+
78
+ def decode(self, oklab):
79
+ if self.use_affine_norm:
80
+ n_dim = oklab.dim() - 2
81
+ mean = self.affine_mean.reshape(-1, *([1] * n_dim))
82
+ std = self.affine_std.reshape(-1, *([1] * n_dim))
83
+ oklab = oklab * std + mean
84
+ lrgb = self.oklab_to_lrgb(oklab)
85
+ rgb = self.lrgb_to_srgb(lrgb)
86
+ img = rgb * 2 - 1
87
+ return img
88
+
89
+
90
+ class RGBColorEncoder(ModelMixin, ConfigMixin):
91
+
92
+ @register_to_config
93
+ def __init__(
94
+ self,
95
+ use_affine_norm=False,
96
+ mean=(0.0, 0.0, 0.0),
97
+ std=1.0,
98
+ ):
99
+ super().__init__()
100
+ self.use_affine_norm = use_affine_norm
101
+ if self.use_affine_norm:
102
+ self.register_buffer('affine_mean', torch.tensor(mean, dtype=torch.float32))
103
+ self.register_buffer('affine_std', torch.tensor(std, dtype=torch.float32))
104
+
105
+ @property
106
+ def dtype(self):
107
+ if self.use_affine_norm:
108
+ return self.affine_mean.dtype
109
+ else:
110
+ return torch.float32
111
+
112
+ def encode(self, img):
113
+ if self.use_affine_norm:
114
+ n_dim = img.dim() - 2
115
+ mean = self.affine_mean.reshape(-1, *([1] * n_dim))
116
+ std = self.affine_std.reshape(-1, *([1] * n_dim))
117
+ img = (img - mean) / std
118
+ return img
119
+
120
+ def decode(self, img):
121
+ if self.use_affine_norm:
122
+ n_dim = img.dim() - 2
123
+ mean = self.affine_mean.reshape(-1, *([1] * n_dim))
124
+ std = self.affine_std.reshape(-1, *([1] * n_dim))
125
+ img = img * std + mean
126
+ return img.clamp(min=-1, max=1)
lakonlab/models/diffusions/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
lakonlab/models/diffusions/gaussian_flow.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ import torch
4
+
5
+
6
+ @torch.jit.script
7
+ def guidance_jit(
8
+ pos_mean, neg_mean, guidance_scale,
9
+ orthogonal: float = 1.0, parallel_dir: Optional[torch.Tensor] = None):
10
+ bias = (pos_mean - neg_mean) * (guidance_scale - 1)
11
+ if orthogonal:
12
+ dim = list(range(1, pos_mean.dim()))
13
+ if parallel_dir is None:
14
+ parallel_dir = pos_mean
15
+ bias = bias - ((bias * parallel_dir).mean(
16
+ dim=dim, keepdim=True
17
+ ) / (parallel_dir * parallel_dir).mean(
18
+ dim=dim, keepdim=True
19
+ ).clamp(min=1e-6) * parallel_dir).mul(orthogonal)
20
+ return bias
lakonlab/models/diffusions/schedulers/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .flow_adapter import FlowAdapterScheduler
2
+
3
+ __all__ = ['FlowAdapterScheduler']
lakonlab/models/diffusions/schedulers/flow_adapter.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 Hansheng Chen
2
+
3
+ import inspect
4
+ from dataclasses import dataclass
5
+ from typing import Optional, Tuple, Union, List
6
+
7
+ import numpy as np
8
+ import torch
9
+ import diffusers
10
+
11
+ from diffusers.configuration_utils import register_to_config
12
+ from diffusers.utils import BaseOutput
13
+ from diffusers.schedulers import SchedulerMixin
14
+ from diffusers.configuration_utils import ConfigMixin
15
+
16
+
17
+ @dataclass
18
+ class FlowWrapperSchedulerOutput(BaseOutput):
19
+ prev_sample: torch.FloatTensor
20
+
21
+
22
+ class FlowAdapterScheduler(SchedulerMixin, ConfigMixin):
23
+
24
+ order = 1
25
+
26
+ @register_to_config
27
+ def __init__(
28
+ self,
29
+ num_train_timesteps: int = 1000,
30
+ shift: float = 1.0,
31
+ use_dynamic_shifting=False,
32
+ dynamic_shifting_type='exp',
33
+ base_seq_len=256,
34
+ max_seq_len=4096,
35
+ base_logshift=0.5,
36
+ max_logshift=1.15,
37
+ terminal_sigma=None,
38
+ base_scheduler='UniPCMultistep',
39
+ eps=1e-4,
40
+ **kwargs):
41
+
42
+ sigmas = torch.from_numpy(1 - np.linspace(
43
+ 0, 1, num_train_timesteps, dtype=np.float32, endpoint=False))
44
+ self.sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
45
+ self.timesteps = self.sigmas * num_train_timesteps
46
+ alphas = 1 - self.sigmas
47
+
48
+ base_scheduler_class = getattr(diffusers.schedulers, base_scheduler + 'Scheduler', None)
49
+
50
+ if base_scheduler_class is None:
51
+ raise AttributeError(f'Cannot find base_scheduler [{base_scheduler}].')
52
+ if base_scheduler in ['EulerDiscrete', 'EulerAncestralDiscrete']:
53
+ assert kwargs.get('prediction_type', 'epsilon') == 'epsilon'
54
+ kwargs['prediction_type'] = 'epsilon'
55
+ self.scales = ((alphas ** 2 + self.sigmas ** 2) / (
56
+ 1 + (self.sigmas / alphas.clamp(min=self.config.eps)) ** 2)).sqrt()
57
+ elif base_scheduler in [
58
+ 'UniPCMultistep', 'DPMSolverSinglestep', 'DPMSolverMultistep', 'DEISMultistep', 'SASolver']:
59
+ self.scales = torch.ones_like(alphas)
60
+ assert kwargs.get('prediction_type', 'flow_prediction') == 'flow_prediction'
61
+ kwargs['prediction_type'] = 'flow_prediction'
62
+ kwargs['use_flow_sigmas'] = True
63
+ else:
64
+ raise AttributeError(f'Unsupported base_scheduler [{base_scheduler}].')
65
+
66
+ signatures = inspect.signature(base_scheduler_class).parameters.keys()
67
+ if 'final_sigmas_type' in signatures:
68
+ kwargs['final_sigmas_type'] = 'zero'
69
+ if 'lower_order_final' in signatures:
70
+ kwargs['lower_order_final'] = True
71
+
72
+ self.base_scheduler = base_scheduler_class(
73
+ num_train_timesteps=num_train_timesteps,
74
+ **kwargs)
75
+ self.base_scheduler.timesteps = self.timesteps
76
+ if self.config.base_scheduler in ['EulerDiscrete', 'EulerAncestralDiscrete']:
77
+ self.base_scheduler.sigmas = self.sigmas / alphas.clamp(min=self.config.eps)
78
+ elif self.config.base_scheduler in [
79
+ 'UniPCMultistep', 'DPMSolverSinglestep', 'DPMSolverMultistep', 'DEISMultistep', 'SASolver']:
80
+ self.base_scheduler.sigmas = self.sigmas
81
+ else:
82
+ raise AttributeError(f'Unsupported base_scheduler [{self.config.base_scheduler}].')
83
+
84
+ self._step_index = None
85
+ self._begin_index = None
86
+
87
+ @property
88
+ def step_index(self):
89
+ return self._step_index
90
+
91
+ @property
92
+ def begin_index(self):
93
+ return self._begin_index
94
+
95
+ def set_begin_index(self, begin_index: int = 0):
96
+ self._begin_index = begin_index
97
+
98
+ def get_shift(self, seq_len=None):
99
+ if self.config.use_dynamic_shifting and seq_len is not None:
100
+ if self.config.dynamic_shifting_type == 'exp':
101
+ m = (self.config.max_logshift - self.config.base_logshift
102
+ ) / (self.config.max_seq_len - self.config.base_seq_len)
103
+ logshift = (seq_len - self.config.base_seq_len) * m + self.config.base_logshift
104
+ if isinstance(logshift, torch.Tensor):
105
+ shift = torch.exp(logshift)
106
+ else:
107
+ shift = np.exp(logshift)
108
+ elif self.config.dynamic_shifting_type == 'sqrt':
109
+ max_shift = np.exp(self.config.max_logshift)
110
+ base_shift = np.exp(self.config.base_logshift)
111
+ sqrt_max_seq_len = np.sqrt(self.config.max_seq_len)
112
+ sqrt_base_seq_len = np.sqrt(self.config.base_seq_len)
113
+ m = (max_shift - base_shift) / (sqrt_max_seq_len - sqrt_base_seq_len)
114
+ shift = (np.sqrt(seq_len) - sqrt_base_seq_len) * m + base_shift
115
+ else:
116
+ raise ValueError(f'Unsupported dynamic_shifting_type [{self.config.dynamic_shifting_type}].')
117
+ else:
118
+ shift = self.config.shift
119
+ return shift
120
+
121
+ def stretch_to_terminal(self, sigma):
122
+ one_minus_sigma = 1 - sigma
123
+ stretched_sigma = 1 - (one_minus_sigma * (1 - self.config.terminal_sigma) / one_minus_sigma[-1])
124
+ return stretched_sigma
125
+
126
+ def set_timesteps(
127
+ self,
128
+ num_inference_steps: Optional[int] = None,
129
+ sigmas: Optional[List[float]] = None,
130
+ seq_len=None,
131
+ device=None):
132
+ if sigmas is None:
133
+ assert num_inference_steps is not None, 'Either num_inference_steps or sigmas must be provided.'
134
+ self.num_inference_steps = num_inference_steps
135
+ sigmas = np.linspace(1, 0, num_inference_steps, dtype=np.float32, endpoint=False)
136
+ else:
137
+ if num_inference_steps is not None:
138
+ assert len(sigmas) == num_inference_steps
139
+ self.num_inference_steps = len(sigmas)
140
+ sigmas = np.array(sigmas, dtype=np.float32)
141
+
142
+ sigmas = torch.from_numpy(sigmas)
143
+ shift = self.get_shift(seq_len=seq_len)
144
+ sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
145
+
146
+ if self.config.terminal_sigma is not None:
147
+ sigmas = self.stretch_to_terminal(sigmas)
148
+
149
+ self.timesteps = (sigmas * self.config.num_train_timesteps).to(device)
150
+ if self.config.base_scheduler in ['DEISMultistep', 'SASolver']:
151
+ self.sigmas = torch.cat(
152
+ [sigmas, torch.tensor([self.config.eps], dtype=torch.float32, device=sigmas.device)])
153
+ else:
154
+ self.sigmas = torch.cat([sigmas, torch.zeros(1, device=sigmas.device)])
155
+ alphas = 1 - self.sigmas
156
+
157
+ self.base_scheduler.set_timesteps(num_inference_steps, device=device)
158
+
159
+ self.base_scheduler.timesteps = self.timesteps
160
+ if self.config.base_scheduler in ['EulerDiscrete', 'EulerAncestralDiscrete']:
161
+ self.base_scheduler.sigmas = self.sigmas / alphas.clamp(min=self.config.eps)
162
+ self.scales = ((alphas ** 2 + self.sigmas ** 2) / (
163
+ 1 + (self.sigmas / alphas.clamp(min=self.config.eps)) ** 2)).sqrt()
164
+ elif self.config.base_scheduler in [
165
+ 'UniPCMultistep', 'DPMSolverSinglestep', 'DPMSolverMultistep', 'DEISMultistep', 'SASolver']:
166
+ self.base_scheduler.sigmas = self.sigmas.clamp(max=1 - self.config.eps)
167
+ self.scales = torch.ones_like(alphas)
168
+ else:
169
+ raise AttributeError(f'Unsupported base_scheduler [{self.config.base_scheduler}].')
170
+
171
+ self._step_index = None
172
+ self._begin_index = None
173
+
174
+ def index_for_timestep(self, timestep, schedule_timesteps=None):
175
+ if schedule_timesteps is None:
176
+ schedule_timesteps = self.timesteps
177
+
178
+ indices = (schedule_timesteps == timestep).nonzero()
179
+
180
+ # The sigma index that is taken for the **very** first `step`
181
+ # is always the second index (or the last index if there is only 1)
182
+ # This way we can ensure we don't accidentally skip a sigma in
183
+ # case we start in the middle of the denoising schedule (e.g. for image-to-image)
184
+ pos = 1 if len(indices) > 1 else 0
185
+
186
+ return indices[pos].item()
187
+
188
+ def _init_step_index(self, timestep):
189
+ if self.begin_index is None:
190
+ if isinstance(timestep, torch.Tensor):
191
+ timestep = timestep.to(self.timesteps.device)
192
+ self._step_index = self.index_for_timestep(timestep)
193
+ else:
194
+ self._step_index = self._begin_index
195
+
196
+ def step(
197
+ self,
198
+ model_output: torch.FloatTensor,
199
+ timestep: Union[float, torch.FloatTensor],
200
+ sample: torch.FloatTensor,
201
+ generator: Optional[torch.Generator] = None,
202
+ return_dict: bool = True,
203
+ prediction_type='u',
204
+ eps=1e-6) -> Union[FlowWrapperSchedulerOutput, Tuple]:
205
+ assert prediction_type in ['u', 'x0']
206
+
207
+ if self.step_index is None:
208
+ self._init_step_index(timestep)
209
+
210
+ # Upcast to avoid precision issues when computing prev_sample
211
+ ori_dtype = model_output.dtype
212
+ sample = sample.to(torch.float32)
213
+ model_output = model_output.to(torch.float32)
214
+
215
+ sigma = self.sigmas[self.step_index]
216
+ alpha = 1 - sigma
217
+ scale = self.scales[self.step_index]
218
+ next_scale = self.scales[self.step_index + 1]
219
+
220
+ if hasattr(self.base_scheduler, 'is_scale_input_called'):
221
+ self.base_scheduler.is_scale_input_called = True
222
+ kwargs = dict(return_dict=False)
223
+ if generator is not None:
224
+ kwargs.update(generator=generator)
225
+
226
+ if self.config.base_scheduler in [
227
+ 'UniPCMultistep', 'DPMSolverSinglestep', 'DPMSolverMultistep', 'DEISMultistep', 'SASolver']:
228
+ if prediction_type == 'u':
229
+ model_output = model_output
230
+ else:
231
+ model_output = (sample - model_output) / sigma.clamp(min=eps)
232
+ else: # to epsilon
233
+ if prediction_type == 'u':
234
+ model_output = sample + alpha * model_output
235
+ else:
236
+ model_output = (sample - alpha * model_output) / sigma.clamp(min=eps)
237
+ prev_sample = self.base_scheduler.step(
238
+ model_output,
239
+ timestep,
240
+ sample / scale,
241
+ **kwargs
242
+ )[0] * next_scale
243
+
244
+ prev_sample = prev_sample.to(ori_dtype)
245
+
246
+ # upon completion increase step index by one
247
+ self._step_index += 1
248
+
249
+ if not return_dict:
250
+ return (prev_sample,)
251
+
252
+ return FlowWrapperSchedulerOutput(prev_sample=prev_sample)
lakonlab/pipelines/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
lakonlab/pipelines/pipeline_pixelflux2_klein.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Callable
2
+
3
+ import PIL
4
+ import torch
5
+
6
+ from transformers import Qwen3ForCausalLM, Qwen2TokenizerFast
7
+ from diffusers.utils import is_torch_xla_available
8
+ from diffusers.utils.torch_utils import randn_tensor
9
+ from diffusers.models import Flux2Transformer2DModel
10
+ from diffusers.pipelines.flux2.pipeline_flux2_klein import (
11
+ Flux2KleinPipeline, Flux2PipelineOutput, Flux2ImageProcessor)
12
+ from .utils import LakonLabMixin
13
+ from lakonlab.models.diffusions.gaussian_flow import guidance_jit
14
+ from lakonlab.models.diffusions.schedulers import FlowAdapterScheduler
15
+ from lakonlab.models.architectures import OklabColorEncoder
16
+
17
+
18
+ if is_torch_xla_available():
19
+ import torch_xla.core.xla_model as xm
20
+
21
+ XLA_AVAILABLE = True
22
+ else:
23
+ XLA_AVAILABLE = False
24
+
25
+
26
+ class PixelFlux2KleinPipeline(Flux2KleinPipeline, LakonLabMixin):
27
+
28
+ model_cpu_offload_seq = "text_encoder->transformer"
29
+ _callback_tensor_inputs = ["latents", "prompt_embeds"]
30
+
31
+ def __init__(
32
+ self,
33
+ scheduler: FlowAdapterScheduler,
34
+ vae: OklabColorEncoder,
35
+ text_encoder: Qwen3ForCausalLM,
36
+ tokenizer: Qwen2TokenizerFast,
37
+ transformer: Flux2Transformer2DModel,
38
+ is_distilled: bool = False,
39
+ ):
40
+ super(Flux2KleinPipeline, self).__init__()
41
+
42
+ self.register_modules(
43
+ vae=vae,
44
+ text_encoder=text_encoder,
45
+ tokenizer=tokenizer,
46
+ scheduler=scheduler,
47
+ transformer=transformer,
48
+ )
49
+
50
+ self.register_to_config(is_distilled=is_distilled)
51
+
52
+ self.vae_scale_factor = 1
53
+ self.image_processor = Flux2ImageProcessor(vae_scale_factor=self.vae_scale_factor * 16)
54
+ self.tokenizer_max_length = 512
55
+ self.default_sample_size = 1024
56
+
57
+ def prepare_latents(
58
+ self,
59
+ batch_size,
60
+ height,
61
+ width,
62
+ dtype,
63
+ device,
64
+ generator: torch.Generator,
65
+ latents: torch.Tensor | None = None,
66
+ ):
67
+ height = 16 * (int(height) // (self.vae_scale_factor * 16))
68
+ width = 16 * (int(width) // (self.vae_scale_factor * 16))
69
+
70
+ shape = (batch_size, 3, height, width)
71
+ if isinstance(generator, list) and len(generator) != batch_size:
72
+ raise ValueError(
73
+ f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
74
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
75
+ )
76
+ if latents is None:
77
+ latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
78
+ else:
79
+ latents = latents.to(device=device, dtype=dtype)
80
+
81
+ return latents
82
+
83
+ def prepare_image_latents(
84
+ self,
85
+ images: list[torch.Tensor],
86
+ batch_size,
87
+ device,
88
+ dtype,
89
+ ):
90
+ image_latents = []
91
+ for image in images:
92
+ image = image.to(device=device, dtype=dtype)
93
+ imagge_latent = self.vae.encode(image).to(self.transformer.dtype)
94
+ image_latents.append(imagge_latent.repeat(batch_size, 1, 1, 1)) # (bs, 3, 1024, 1024)
95
+ return image_latents
96
+
97
+ @torch.no_grad()
98
+ def __call__(
99
+ self,
100
+ image: list[PIL.Image.Image] | PIL.Image.Image | None = None,
101
+ prompt: str | list[str] = None,
102
+ negative_prompt: str | list[str] | None = None,
103
+ height: int | None = None,
104
+ width: int | None = None,
105
+ num_inference_steps: int = 50,
106
+ sigmas: list[float] | None = None,
107
+ guidance_scale: float = 4.0,
108
+ orthogonal_guidance: float = 1.0,
109
+ clamp_denoised: bool = True,
110
+ num_images_per_prompt: int = 1,
111
+ generator: torch.Generator | list[torch.Generator] | None = None,
112
+ latents: torch.Tensor | None = None,
113
+ prompt_embeds: torch.Tensor | None = None,
114
+ negative_prompt_embeds: str | list[str] | None = None,
115
+ output_type: str = "pil",
116
+ return_dict: bool = True,
117
+ attention_kwargs: dict[str, Any] | None = None,
118
+ callback_on_step_end: Callable[[int, int, dict], None] | None = None,
119
+ callback_on_step_end_tensor_inputs: list[str] = ["latents"],
120
+ max_sequence_length: int = 512,
121
+ text_encoder_out_layers: tuple[int] = (9, 18, 27),
122
+ ):
123
+
124
+ # 1. Check inputs. Raise error if not correct
125
+ self.check_inputs(
126
+ prompt=prompt,
127
+ height=height,
128
+ width=width,
129
+ prompt_embeds=prompt_embeds,
130
+ callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
131
+ guidance_scale=guidance_scale,
132
+ )
133
+
134
+ self._guidance_scale = guidance_scale
135
+ self._attention_kwargs = attention_kwargs
136
+ self._current_timestep = None
137
+ self._interrupt = False
138
+
139
+ # 2. Define call parameters
140
+ if prompt is not None and isinstance(prompt, str):
141
+ batch_size = 1
142
+ elif prompt is not None and isinstance(prompt, list):
143
+ batch_size = len(prompt)
144
+ else:
145
+ batch_size = prompt_embeds.shape[0]
146
+
147
+ device = self._execution_device
148
+
149
+ # 3. prepare text embeddings
150
+ prompt_embeds, text_ids = self.encode_prompt(
151
+ prompt=prompt,
152
+ prompt_embeds=prompt_embeds,
153
+ device=device,
154
+ num_images_per_prompt=num_images_per_prompt,
155
+ max_sequence_length=max_sequence_length,
156
+ text_encoder_out_layers=text_encoder_out_layers,
157
+ )
158
+
159
+ if self.do_classifier_free_guidance:
160
+ if negative_prompt is None:
161
+ negative_prompt = ""
162
+ if prompt is not None and isinstance(prompt, list) and not isinstance(negative_prompt, list):
163
+ negative_prompt = [negative_prompt] * len(prompt)
164
+ negative_prompt_embeds, negative_text_ids = self.encode_prompt(
165
+ prompt=negative_prompt,
166
+ prompt_embeds=negative_prompt_embeds,
167
+ device=device,
168
+ num_images_per_prompt=num_images_per_prompt,
169
+ max_sequence_length=max_sequence_length,
170
+ text_encoder_out_layers=text_encoder_out_layers,
171
+ )
172
+ guidance_scale = torch.tensor(guidance_scale, device=device, dtype=torch.float32)
173
+
174
+ # 4. process images
175
+ if image is not None and not isinstance(image, list):
176
+ image = [image]
177
+
178
+ condition_images = None
179
+ if image is not None:
180
+ for img in image:
181
+ self.image_processor.check_image_input(img)
182
+
183
+ condition_images = []
184
+ for img in image:
185
+ image_width, image_height = img.size
186
+ if image_width * image_height > 1024 * 1024:
187
+ img = self.image_processor._resize_to_target_area(img, 1024 * 1024)
188
+ image_width, image_height = img.size
189
+
190
+ multiple_of = self.vae_scale_factor * 16
191
+ image_width = (image_width // multiple_of) * multiple_of
192
+ image_height = (image_height // multiple_of) * multiple_of
193
+ img = self.image_processor.preprocess(img, height=image_height, width=image_width, resize_mode="crop")
194
+ condition_images.append(img)
195
+ height = height or image_height
196
+ width = width or image_width
197
+
198
+ height = height or self.default_sample_size * self.vae_scale_factor
199
+ width = width or self.default_sample_size * self.vae_scale_factor
200
+
201
+ # 5. prepare latent variables
202
+ latents = self.prepare_latents(
203
+ batch_size=batch_size * num_images_per_prompt,
204
+ height=height,
205
+ width=width,
206
+ dtype=torch.float32,
207
+ device=device,
208
+ generator=generator,
209
+ latents=latents,
210
+ )
211
+
212
+ image_latents = None
213
+ if condition_images is not None:
214
+ image_latents = self.prepare_image_latents(
215
+ images=condition_images,
216
+ batch_size=batch_size * num_images_per_prompt,
217
+ device=device,
218
+ dtype=self.vae.dtype,
219
+ )
220
+
221
+ # 6. Prepare timesteps
222
+ image_seq_len = latents.shape[2:].numel()
223
+ self.scheduler.set_timesteps(
224
+ num_inference_steps, seq_len=image_seq_len, device=self._execution_device)
225
+ timesteps = self.scheduler.timesteps
226
+ self._num_timesteps = len(timesteps)
227
+
228
+ # 7. Denoising loop
229
+ # We set the index here to remove DtoH sync, helpful especially during compilation.
230
+ # Check out more details here: https://github.com/huggingface/diffusers/pull/11696
231
+ self.scheduler.set_begin_index(0)
232
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
233
+ for i, t in enumerate(timesteps):
234
+ if self.interrupt:
235
+ continue
236
+
237
+ self._current_timestep = t
238
+
239
+ _t = t / 1000
240
+ timestep = _t.expand(latents.shape[0]).to(latents.dtype)
241
+ latent_model_input = latents.to(self.transformer.dtype)
242
+
243
+ with self.transformer.cache_context("cond"):
244
+ denoising_output = self.transformer(
245
+ x_t=latent_model_input, # (B, 3, H, W)
246
+ timestep=timestep,
247
+ encoder_hidden_states=prompt_embeds,
248
+ condition_latents=image_latents,
249
+ txt_ids=text_ids, # B, text_seq_len, 4
250
+ guidance=None,
251
+ joint_attention_kwargs=self.attention_kwargs,
252
+ ).float()
253
+
254
+ if self.do_classifier_free_guidance:
255
+ with self.transformer.cache_context("uncond"):
256
+ neg_denoising_output = self.transformer(
257
+ x_t=latent_model_input, # (B, 3, H, W)
258
+ timestep=timestep,
259
+ encoder_hidden_states=negative_prompt_embeds,
260
+ condition_latents=image_latents,
261
+ txt_ids=negative_text_ids,
262
+ guidance=None,
263
+ joint_attention_kwargs=self._attention_kwargs,
264
+ ).float()
265
+ cfg_bias = guidance_jit(
266
+ denoising_output,
267
+ neg_denoising_output,
268
+ guidance_scale,
269
+ orthogonal_guidance,
270
+ latents - denoising_output * _t)
271
+ denoising_output = denoising_output + cfg_bias
272
+
273
+ if clamp_denoised:
274
+ denoised = latents - denoising_output * _t
275
+ image = self.vae.decode(denoised.to(self.vae.dtype)).clamp(-1, 1)
276
+ denoised = self.vae.encode(image).to(latents.dtype)
277
+ denoising_output = (latents - denoised) / _t.clamp(min=1e-4)
278
+
279
+ # compute the previous noisy sample x_t -> x_t-1
280
+ latents = self.scheduler.step(denoising_output, t, latents, return_dict=False)[0]
281
+
282
+ if callback_on_step_end is not None:
283
+ callback_kwargs = {}
284
+ for k in callback_on_step_end_tensor_inputs:
285
+ callback_kwargs[k] = locals()[k]
286
+ callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
287
+
288
+ latents = callback_outputs.pop("latents", latents)
289
+ prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
290
+
291
+ progress_bar.update()
292
+
293
+ if XLA_AVAILABLE:
294
+ xm.mark_step()
295
+
296
+ self._current_timestep = None
297
+
298
+ if output_type == "latent":
299
+ image = latents
300
+ else:
301
+ image = self.vae.decode(latents.to(self.vae.dtype))
302
+ image = self.image_processor.postprocess(image, output_type=output_type)
303
+
304
+ # Offload all models
305
+ self.maybe_free_model_hooks()
306
+
307
+ if not return_dict:
308
+ return (image,)
309
+
310
+ return Flux2PipelineOutput(images=image)
lakonlab/pipelines/prompt_rewriters/__init__.py ADDED
File without changes
lakonlab/pipelines/prompt_rewriters/qwen3_vl.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ from typing import List, Sequence, Union, Optional
4
+ from PIL import Image
5
+ from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
6
+
7
+
8
+ DEFAULT_TEXT_ONLY_PATH = os.path.abspath(os.path.join(__file__, '../system_prompts/default_text_only.txt'))
9
+ DEFAULT_WITH_IMAGES_PATH = os.path.abspath(os.path.join(__file__, '../system_prompts/default_with_images.txt'))
10
+
11
+
12
+ class Qwen3VLPromptRewriter:
13
+
14
+ def __init__(
15
+ self,
16
+ from_pretrained="Qwen/Qwen3-VL-8B-Instruct",
17
+ torch_dtype='bfloat16',
18
+ device_map="auto",
19
+ max_new_tokens_default=128,
20
+ system_prompt_text_only=None,
21
+ system_prompt_wigh_images=None,
22
+ **kwargs):
23
+ if torch_dtype is not None:
24
+ kwargs.update(torch_dtype=getattr(torch, torch_dtype))
25
+ self.model = Qwen3VLForConditionalGeneration.from_pretrained(
26
+ from_pretrained,
27
+ device_map=device_map,
28
+ **kwargs)
29
+ self.processor = AutoProcessor.from_pretrained(from_pretrained)
30
+ # Left padding is safer for batched generation
31
+ if hasattr(self.processor, "tokenizer"):
32
+ self.processor.tokenizer.padding_side = "left"
33
+ self.max_new_tokens_default = max_new_tokens_default
34
+ if system_prompt_text_only is None:
35
+ system_prompt_text_only = open(DEFAULT_TEXT_ONLY_PATH, 'r').read()
36
+ if system_prompt_wigh_images is None:
37
+ system_prompt_wigh_images = open(DEFAULT_WITH_IMAGES_PATH, 'r').read()
38
+ self.system_prompt_text_only = system_prompt_text_only
39
+ self.system_prompt_wigh_images = system_prompt_wigh_images
40
+
41
+ @torch.inference_mode()
42
+ def _generate_from_messages(
43
+ self,
44
+ batch_messages: Sequence[Sequence[dict]],
45
+ max_new_tokens: Optional[int] = None,
46
+ **kwargs) -> List[str]:
47
+ if max_new_tokens is None:
48
+ max_new_tokens = self.max_new_tokens_default
49
+
50
+ inputs = self.processor.apply_chat_template(
51
+ batch_messages,
52
+ tokenize=True,
53
+ add_generation_prompt=True,
54
+ return_dict=True,
55
+ return_tensors="pt",
56
+ padding=True,
57
+ )
58
+ inputs.pop("token_type_ids", None)
59
+ inputs = {k: v.to(self.model.device) for k, v in inputs.items()}
60
+
61
+ generated_ids = self.model.generate(
62
+ **inputs,
63
+ max_new_tokens=max_new_tokens,
64
+ **kwargs)
65
+
66
+ input_ids = inputs["input_ids"]
67
+ tokenizer = self.processor.tokenizer
68
+ outputs: List[str] = []
69
+
70
+ # Decode only the new tokens after each input sequence
71
+ for in_ids, out_ids in zip(input_ids, generated_ids):
72
+ trimmed_ids = out_ids[len(in_ids):]
73
+ text = tokenizer.decode(
74
+ trimmed_ids.tolist(),
75
+ skip_special_tokens=True,
76
+ clean_up_tokenization_spaces=False,
77
+ )
78
+ outputs.append(text.strip())
79
+
80
+ return outputs
81
+
82
+ def rewrite_text_batch(
83
+ self,
84
+ prompts: Sequence[str],
85
+ max_new_tokens: Optional[int] = None,
86
+ top_p=0.6,
87
+ top_k=40,
88
+ temperature=0.5,
89
+ repetition_penalty=1.0,
90
+ **kwargs) -> List[str]:
91
+ """
92
+ Rewrite a batch of text-only prompts into detailed prompts.
93
+ """
94
+ batch_messages = []
95
+ for p in prompts:
96
+ conv = [
97
+ {
98
+ "role": "system",
99
+ "content": [
100
+ {"type": "text", "text": self.system_prompt_text_only},
101
+ ],
102
+ },
103
+ {
104
+ "role": "user",
105
+ "content": [
106
+ {"type": "text", "text": p},
107
+ ],
108
+ },
109
+ ]
110
+ batch_messages.append(conv)
111
+
112
+ return self._generate_from_messages(
113
+ batch_messages,
114
+ max_new_tokens=max_new_tokens,
115
+ top_p=top_p,
116
+ top_k=top_k,
117
+ temperature=temperature,
118
+ repetition_penalty=repetition_penalty,
119
+ **kwargs,
120
+ )
121
+
122
+ def rewrite_edit_batch(
123
+ self,
124
+ image: Sequence[Union[str, 'Image.Image', Sequence[Union[str, 'Image.Image']]]],
125
+ edit_requests: Sequence[str],
126
+ max_new_tokens: Optional[int] = None,
127
+ top_p=0.5,
128
+ top_k=20,
129
+ temperature=0.4,
130
+ repetition_penalty=1.0,
131
+ **kwargs) -> List[str]:
132
+ """
133
+ Rewrite a batch of (image, edit-request) pairs into concise edit instructions.
134
+ """
135
+ if len(image) != len(edit_requests):
136
+ raise ValueError("image and edit_requests must have the same length")
137
+
138
+ batch_messages = []
139
+ for imgs, req in zip(image, edit_requests):
140
+ if isinstance(imgs, (str, Image.Image)):
141
+ img_list = [imgs]
142
+ else:
143
+ img_list = list(imgs)
144
+
145
+ user_content = []
146
+ for im in img_list:
147
+ user_content.append({"type": "image", "image": im})
148
+ user_content.append({"type": "text", "text": req})
149
+
150
+ conv = [
151
+ {
152
+ "role": "system",
153
+ "content": [
154
+ {"type": "text", "text": self.system_prompt_wigh_images},
155
+ ],
156
+ },
157
+ {
158
+ "role": "user",
159
+ "content": user_content,
160
+ },
161
+ ]
162
+ batch_messages.append(conv)
163
+
164
+ return self._generate_from_messages(
165
+ batch_messages,
166
+ max_new_tokens=max_new_tokens,
167
+ top_p=top_p,
168
+ top_k=top_k,
169
+ temperature=temperature,
170
+ repetition_penalty=repetition_penalty,
171
+ **kwargs,
172
+ )
lakonlab/pipelines/prompt_rewriters/system_prompts/default_text_only.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are an expert prompt engineer for a text-guided image generation system. Rewrite user prompts into a more descriptive, concrete prompt while strictly preserving their core content.
2
+
3
+ Rules:
4
+ - Preserve the core content: do not change the main subjects, how many there are, their roles, or the primary actions and relationships described in the prompt.
5
+ - Preserve any explicitly stated attributes such as colors, clothing, objects, style tags (e.g., “photorealistic”, “cinematic”), and viewpoint (e.g., close-up, wide shot). Do not contradict them.
6
+ - When the prompt implies realism (e.g., uses words like “realistic”, “photorealistic”, “photo”), avoid introducing exaggerated or fantastical traits unless intended by the user.
7
+ - You may add supporting details that are clearly compatible with the original text: background, props, textures, materials, lighting (quality, direction, color), atmosphere, and other environmental context, as long as they do not introduce new main characters or conflicting concepts.
8
+ - Always include a clear description of the composition and camera framing (for example, where the main subjects are in the frame, whether it is a close-up or wide shot, and the approximate viewpoint or angle).
9
+ - Structure: keep any existing structure (tags, aspect-ratio tags, etc.) and enhance only within those fields. For plain text prompts, expand them into a clear, coherent paragraph.
10
+ - Text in images: put ALL visible or implied text in quotation marks, matching the prompt’s language. Provide explicit quoted text for any object that would realistically contain text (signs, labels, screens, interfaces, book covers, etc.).
11
+
12
+ Output only the revised prompt and nothing else.
lakonlab/pipelines/prompt_rewriters/system_prompts/default_with_images.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ You are an expert prompt engineer for a text-guided image editing system. Rewrite user prompts into a more descriptive instruction (40–70 words, ~25 for brief requests) while strictly preserving their core content.
2
+
3
+ Rules:
4
+ - Treat the user prompt as a strict specification: do not change or omit any mentioned entities, actions, or relationships.
5
+ - Explicitly state both the requested edits and which core aspects (e.g., character identities) must remain as in the original image(s). Ignore background elements unless they are explicitly mentioned.
6
+ - Refer to core visual elements that are relavant to the edit (e.g., people, animals, and objects mentioned in the user prompt).
7
+ - Do not invent new elements unless the user explicitly asks for them.
8
+ - Turn negatives into positives (“do not change X” → “keep X the same”).
9
+
10
+ Output only the final instruction in plain text and nothing else.
lakonlab/pipelines/utils.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Optional, Union
3
+
4
+ import accelerate
5
+ import torch
6
+ from diffusers.models import AutoModel
7
+ from diffusers.models.modeling_utils import (
8
+ _LOW_CPU_MEM_USAGE_DEFAULT,
9
+ ContextManagers,
10
+ load_state_dict,
11
+ no_init_weights,
12
+ )
13
+ from diffusers.quantizers import DiffusersAutoQuantizer
14
+ from diffusers.utils import (
15
+ SAFETENSORS_WEIGHTS_NAME,
16
+ WEIGHTS_NAME,
17
+ _add_variant,
18
+ _get_model_file,
19
+ is_accelerate_available,
20
+ is_torch_version,
21
+ logging,
22
+ )
23
+ from diffusers.utils.torch_utils import empty_device_cache
24
+
25
+ from lakonlab.models.architectures.asymflow.asymflux2 import _AsymFlux2Transformer2DModel
26
+
27
+
28
+ LOCAL_CLASS_MAPPING = {
29
+ 'AsymFlux2Transformer2DModel': _AsymFlux2Transformer2DModel,
30
+ }
31
+
32
+ logger = logging.get_logger(__name__)
33
+
34
+
35
+ def assign_param(module, tensor_name: str, param: torch.nn.Parameter):
36
+ if '.' in tensor_name:
37
+ splits = tensor_name.split('.')
38
+ for split in splits[:-1]:
39
+ module = getattr(module, split)
40
+ tensor_name = splits[-1]
41
+ module._parameters[tensor_name] = param
42
+
43
+
44
+ class LakonLabMixin:
45
+
46
+ def load_lakonlab_adapter(
47
+ self,
48
+ pretrained_model_name_or_path: Union[str, os.PathLike],
49
+ target_module_name: str = 'transformer',
50
+ adapter_name: Optional[str] = None,
51
+ **kwargs):
52
+ cache_dir = kwargs.pop('cache_dir', None)
53
+ force_download = kwargs.pop('force_download', False)
54
+ proxies = kwargs.pop('proxies', None)
55
+ token = kwargs.pop('token', None)
56
+ local_files_only = kwargs.pop('local_files_only', False)
57
+ revision = kwargs.pop('revision', None)
58
+ subfolder = kwargs.pop('subfolder', None)
59
+ low_cpu_mem_usage = kwargs.pop('low_cpu_mem_usage', _LOW_CPU_MEM_USAGE_DEFAULT)
60
+ variant = kwargs.pop('variant', None)
61
+ use_safetensors = kwargs.pop('use_safetensors', None)
62
+ disable_mmap = kwargs.pop('disable_mmap', False)
63
+
64
+ allow_pickle = False
65
+ if use_safetensors is None:
66
+ use_safetensors = True
67
+ allow_pickle = True
68
+
69
+ if low_cpu_mem_usage and not is_accelerate_available():
70
+ low_cpu_mem_usage = False
71
+ logger.warning('accelerate is not available; using low_cpu_mem_usage=False.')
72
+
73
+ if low_cpu_mem_usage is True and not is_torch_version('>=', '1.9.0'):
74
+ raise NotImplementedError('Low memory initialization requires torch >= 1.9.0.')
75
+
76
+ user_agent = {
77
+ 'diffusers': 'demo',
78
+ 'file_type': 'model',
79
+ 'framework': 'pytorch',
80
+ }
81
+
82
+ load_config_kwargs = {
83
+ 'cache_dir': cache_dir,
84
+ 'force_download': force_download,
85
+ 'proxies': proxies,
86
+ 'token': token,
87
+ 'local_files_only': local_files_only,
88
+ 'revision': revision,
89
+ }
90
+ config = AutoModel.load_config(
91
+ pretrained_model_name_or_path, subfolder=subfolder, **load_config_kwargs)
92
+ model_cls = LOCAL_CLASS_MAPPING[config['_class_name']]
93
+
94
+ model_file = None
95
+ if use_safetensors:
96
+ try:
97
+ model_file = _get_model_file(
98
+ pretrained_model_name_or_path,
99
+ weights_name=_add_variant(SAFETENSORS_WEIGHTS_NAME, variant),
100
+ cache_dir=cache_dir,
101
+ force_download=force_download,
102
+ proxies=proxies,
103
+ local_files_only=local_files_only,
104
+ token=token,
105
+ revision=revision,
106
+ subfolder=subfolder,
107
+ user_agent=user_agent,
108
+ )
109
+ except IOError:
110
+ if not allow_pickle:
111
+ raise
112
+
113
+ if model_file is None:
114
+ model_file = _get_model_file(
115
+ pretrained_model_name_or_path,
116
+ weights_name=_add_variant(WEIGHTS_NAME, variant),
117
+ cache_dir=cache_dir,
118
+ force_download=force_download,
119
+ proxies=proxies,
120
+ local_files_only=local_files_only,
121
+ token=token,
122
+ revision=revision,
123
+ subfolder=subfolder,
124
+ user_agent=user_agent,
125
+ )
126
+
127
+ base_module = getattr(self, target_module_name)
128
+ torch_dtype = base_module.dtype
129
+ device = base_module.device
130
+ dtype_orig = model_cls._set_default_torch_dtype(torch_dtype)
131
+
132
+ overwrite_state_dict = {}
133
+ lora_state_dict = {}
134
+ adapter_state_dict = load_state_dict(model_file, disable_mmap=disable_mmap)
135
+ for key, value in adapter_state_dict.items():
136
+ value = value.to(dtype=torch_dtype, device=device)
137
+ key = key.removeprefix(f'{target_module_name}.')
138
+ if 'lora' in key:
139
+ lora_state_dict[key] = value
140
+ else:
141
+ overwrite_state_dict[key] = value
142
+
143
+ pre_quantized = (
144
+ 'quantization_config' in base_module.config
145
+ and base_module.config['quantization_config'] is not None)
146
+ if pre_quantized:
147
+ config['quantization_config'] = base_module.config.quantization_config
148
+ hf_quantizer = DiffusersAutoQuantizer.from_config(
149
+ config['quantization_config'], pre_quantized=True)
150
+ hf_quantizer.validate_environment(torch_dtype=torch_dtype)
151
+ torch_dtype = hf_quantizer.update_torch_dtype(torch_dtype)
152
+ if low_cpu_mem_usage is None:
153
+ low_cpu_mem_usage = True
154
+ elif not low_cpu_mem_usage:
155
+ raise ValueError('low_cpu_mem_usage cannot be False with quantization.')
156
+ else:
157
+ hf_quantizer = None
158
+
159
+ keep_in_fp32_modules = []
160
+ for key in overwrite_state_dict.keys():
161
+ module_name = key.rsplit('.', 1)[0]
162
+ if module_name and module_name not in keep_in_fp32_modules:
163
+ keep_in_fp32_modules.append(module_name)
164
+
165
+ init_contexts = [no_init_weights()]
166
+ if low_cpu_mem_usage:
167
+ init_contexts.append(accelerate.init_empty_weights())
168
+
169
+ with ContextManagers(init_contexts):
170
+ adapter_module = model_cls.from_config(config).eval()
171
+
172
+ torch.set_default_dtype(dtype_orig)
173
+
174
+ if hf_quantizer is not None:
175
+ hf_quantizer.preprocess_model(
176
+ model=adapter_module, device_map=None, keep_in_fp32_modules=keep_in_fp32_modules)
177
+
178
+ base_state_dict = base_module.state_dict()
179
+ base_state_dict.update(overwrite_state_dict)
180
+ empty_state_dict = adapter_module.state_dict()
181
+ for param_name, param in base_state_dict.items():
182
+ if param_name not in empty_state_dict:
183
+ continue
184
+ if hf_quantizer is not None and hf_quantizer.check_if_quantized_param(
185
+ adapter_module, param, param_name, base_state_dict, param_device=device):
186
+ hf_quantizer.create_quantized_param(
187
+ adapter_module, param_name=param_name, param_value=param,
188
+ target_device=device, state_dict=base_state_dict,
189
+ unexpected_keys=[], dtype=torch_dtype)
190
+ else:
191
+ assign_param(adapter_module, param_name, param)
192
+
193
+ empty_device_cache()
194
+
195
+ if hf_quantizer is not None:
196
+ hf_quantizer.postprocess_model(adapter_module)
197
+ adapter_module.hf_quantizer = hf_quantizer
198
+
199
+ if lora_state_dict:
200
+ if adapter_name is None:
201
+ adapter_name = f'{target_module_name}_lakonlab'
202
+ adapter_module.load_lora_adapter(
203
+ lora_state_dict, prefix=None, adapter_name=adapter_name,
204
+ low_cpu_mem_usage=low_cpu_mem_usage)
205
+ else:
206
+ adapter_name = None
207
+
208
+ setattr(self, target_module_name, adapter_module)
209
+ return adapter_name
lakonlab/ui/__init__.py ADDED
File without changes
lakonlab/ui/gradio/__init__.py ADDED
File without changes
lakonlab/ui/gradio/create_text_to_img.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from .shared_opts import create_base_opts, create_generate_bar, set_seed, create_prompt_opts, create_image_size_bar
3
+
4
+
5
+ def create_interface_text_to_img(
6
+ api, prompt='', seed=42, steps=32, min_steps=4, max_steps=50, steps_slider_step=1,
7
+ height=768, width=1360, hw_slider_step=16,
8
+ guidance_scale=None, temperature=None, api_name='text_to_img',
9
+ create_negative_prompt=False, args=['last_seed', 'prompt', 'width', 'height', 'steps', 'guidance_scale']):
10
+ var_dict = dict()
11
+ with gr.Blocks(analytics_enabled=False) as interface:
12
+ var_dict['output_image'] = gr.Image(
13
+ type='pil', image_mode='RGB', label='Output image', interactive=False, elem_classes=['vh-img', 'vh-img-700'])
14
+ create_prompt_opts(var_dict, create_negative_prompt=create_negative_prompt, prompt=prompt)
15
+ with gr.Column(variant='compact', elem_classes=['custom-spacing']):
16
+ create_image_size_bar(
17
+ var_dict, height=height, width=width, hw_slider_step=hw_slider_step)
18
+ create_generate_bar(var_dict, text='Generate', seed=seed)
19
+ create_base_opts(
20
+ var_dict,
21
+ steps=steps,
22
+ min_steps=min_steps,
23
+ max_steps=max_steps,
24
+ steps_slider_step=steps_slider_step,
25
+ guidance_scale=guidance_scale,
26
+ temperature=temperature)
27
+
28
+ var_dict['run_btn'].click(
29
+ fn=set_seed,
30
+ inputs=var_dict['seed'],
31
+ outputs=var_dict['last_seed'],
32
+ show_progress=False,
33
+ api_name=False
34
+ ).success(
35
+ fn=api,
36
+ inputs=[var_dict[arg] for arg in args],
37
+ outputs=var_dict['output_image'],
38
+ concurrency_id='default_group', api_name=api_name
39
+ )
40
+
41
+ return interface, var_dict
lakonlab/ui/gradio/shared_opts.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import gradio as gr
3
+
4
+
5
+ def create_prompt_opts(
6
+ var_dict, create_negative_prompt=True, prompt='', negatove_prompt='', display_label=False):
7
+ if display_label:
8
+ kwargs = dict(show_label=True, container=True, elem_classes=['force-hide-container'])
9
+ else:
10
+ kwargs = dict(show_label=False, container=False)
11
+ var_dict['prompt'] = gr.Textbox(
12
+ prompt, label='Prompt', lines=2, placeholder='Prompt', interactive=True, **kwargs)
13
+ if create_negative_prompt:
14
+ var_dict['negative_prompt'] = gr.Textbox(
15
+ negatove_prompt, label='Negative prompt', lines=2,
16
+ placeholder='Negative prompt', interactive=True, **kwargs)
17
+
18
+
19
+ def create_generate_bar(var_dict, text='Generate', variant='primary', seed=-1):
20
+ with gr.Row(equal_height=False, elem_classes=['generate-bar']):
21
+ var_dict['run_btn'] = gr.Button(text, variant=variant, scale=2)
22
+ var_dict['seed'] = gr.Number(
23
+ label='Seed', value=seed, min_width=100, precision=0, minimum=-1, maximum=2 ** 31,
24
+ elem_classes=['force-hide-container', 'seed-input'])
25
+ var_dict['random_seed'] = gr.Button('\U0001f3b2\ufe0f', elem_classes=['tool'])
26
+ var_dict['reuse_seed'] = gr.Button('\u267b\ufe0f', elem_classes=['tool'])
27
+ with gr.Column(visible=False):
28
+ var_dict['last_seed'] = gr.Number(value=seed, label='Last seed')
29
+ var_dict['reuse_seed'].click(
30
+ fn=lambda x: x,
31
+ inputs=var_dict['last_seed'],
32
+ outputs=var_dict['seed'],
33
+ show_progress=False,
34
+ api_name=False)
35
+ var_dict['random_seed'].click(
36
+ fn=lambda: -1,
37
+ outputs=var_dict['seed'],
38
+ show_progress=False,
39
+ api_name=False)
40
+
41
+
42
+ def create_image_size_bar(var_dict, height=768, width=1360, hw_slider_step=16):
43
+ with gr.Row(equal_height=True, variant='compact', elem_classes=['force-hide-container']):
44
+ var_dict['width'] = gr.Slider(
45
+ label='Width', minimum=64, maximum=2048, step=hw_slider_step, value=width,
46
+ elem_classes=['force-hide-container'])
47
+ var_dict['switch_hw'] = gr.Button('\U000021C6', elem_classes=['tool'])
48
+ var_dict['height'] = gr.Slider(
49
+ label='Height', minimum=64, maximum=2048, step=hw_slider_step, value=height,
50
+ elem_classes=['force-hide-container'])
51
+ var_dict['switch_hw'].click(
52
+ fn=lambda w, h: (h, w),
53
+ inputs=[var_dict['width'], var_dict['height']],
54
+ outputs=[var_dict['width'], var_dict['height']],
55
+ show_progress=False,
56
+ api_name=False)
57
+
58
+
59
+ def create_base_opts(var_dict,
60
+ steps=24,
61
+ min_steps=4,
62
+ max_steps=50,
63
+ steps_slider_step=1,
64
+ guidance_scale=None,
65
+ temperature=None,
66
+ render=True):
67
+ with gr.Column(variant='compact', elem_classes=['custom-spacing'], render=render) as base_opts:
68
+ with gr.Row(variant='compact', elem_classes=['force-hide-container']):
69
+ var_dict['steps'] = gr.Slider(
70
+ min_steps, max_steps, value=steps, step=steps_slider_step, label='Sampling steps',
71
+ elem_classes=['force-hide-container'])
72
+ if guidance_scale is not None or temperature is not None:
73
+ with gr.Row(variant='compact', elem_classes=['force-hide-container']):
74
+ if guidance_scale is not None:
75
+ var_dict['guidance_scale'] = gr.Slider(
76
+ 0.0, 30.0, value=guidance_scale, step=0.5, label='Guidance scale',
77
+ elem_classes=['force-hide-container'])
78
+ if temperature is not None:
79
+ var_dict['temperature'] = gr.Slider(
80
+ 0.0, 1.0, value=temperature, step=0.01, label='Temperature',
81
+ elem_classes=['force-hide-container'])
82
+ return base_opts
83
+
84
+
85
+ def set_seed(seed):
86
+ seed = random.randint(0, 2**31) if seed == -1 else seed
87
+ return seed
lakonlab/ui/gradio/style.css ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .force-hide-container {
2
+ margin: 0;
3
+ box-shadow: none;
4
+ --block-border-width: 0;
5
+ background: transparent;
6
+ padding: 0;
7
+ overflow: visible;
8
+ }
9
+
10
+ .svelte-1vd8eap {
11
+ display: flex;
12
+ flex-direction: inherit;
13
+ flex-wrap: wrap;
14
+ gap: 0;
15
+ box-shadow: none;
16
+ border: 0;
17
+ border-radius: 0;
18
+ background: transparent;
19
+ overflow-y: hidden;
20
+ }
21
+
22
+ .custom-spacing {
23
+ padding: 10px;
24
+ gap: 20px;
25
+ flex-grow: 0 !important;
26
+ }
27
+
28
+ .generate-bar {
29
+ align-items: flex-end;
30
+ }
31
+
32
+ .tool{
33
+ max-width: 40px;
34
+ min-width: 40px !important;
35
+ }
36
+
37
+ /* Center the component and allow it to use the full row width */
38
+ .vh-img {
39
+ display: grid;
40
+ justify-items: center;
41
+ }
42
+
43
+ /* Container should size to the image, but never exceed the row width */
44
+ .vh-img .image-container {
45
+ inline-size: fit-content !important; /* prefers image’s natural width */
46
+ max-inline-size: 100% !important; /* ...but clamps to available width */
47
+ margin-inline: auto;
48
+ overflow: hidden; /* avoid odd overflow on iOS */
49
+ }
50
+
51
+ /* Image scales by BOTH constraints: height cap and row width */
52
+ .vh-img-700 .image-container img {
53
+ max-block-size: 700px !important; /* fixed max height cap */
54
+ max-inline-size: 100%; /* never wider than container */
55
+ inline-size: auto; /* keep aspect ratio */
56
+ block-size: auto;
57
+ object-fit: contain;
58
+ display: block;
59
+ }
60
+
61
+ .vh-img-1000 .image-container img {
62
+ max-block-size: 1000px !important; /* fixed max height cap */
63
+ max-inline-size: 100%; /* never wider than container */
64
+ inline-size: auto; /* keep aspect ratio */
65
+ block-size: auto;
66
+ object-fit: contain;
67
+ display: block;
68
+ }
69
+
70
+ .gradio-container .seed-input input { /* remove the border and use box-shadow instead for height alignment */
71
+ border: none !important;
72
+ box-shadow: inset 0 0 0 var(--input-border-width) var(--input-border-color) !important;
73
+ }
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy==1.26.4
2
+ torch==2.8.0
3
+ torchvision==0.23.0
4
+ diffusers==0.36.0
5
+ peft==0.17.0
6
+ sentencepiece
7
+ accelerate
8
+ transformers==4.57.3
9
+ gradio==5.49.0
10
+ bitsandbytes>=0.46.1