mirror of
https://github.com/vladmandic/automatic
synced 2026-09-19 01:04:32 +02:00
+8
-4
@@ -1,11 +1,11 @@
|
||||
# Change Log for SD.Next
|
||||
|
||||
## Update for 2026-05-22
|
||||
## Update for 2026-05-23
|
||||
|
||||
### Highlights for 2026-05-22
|
||||
### Highlights for 2026-05-23
|
||||
|
||||
*What's New?*
|
||||
- **Anima** made it to release version
|
||||
- **Anima** made it to release version, Microsoft joins the game with **Lens**
|
||||
- **SDNQ** new quantization algorithm with even higher quality
|
||||
- New **image analysis** feature and much improved **prompt enhance** capabilities which allow steering the model in real-time
|
||||
- Improved image metadata options
|
||||
@@ -14,11 +14,15 @@ And we have new [Contibuting** & **Development](https://vladmandic.github.io/sdn
|
||||
|
||||
Plus continued work on modernization of codebase: UI is now fully TypeScript based and new modular LoRA loader
|
||||
|
||||
### Details for 2026-05-22
|
||||
### Details for 2026-05-23
|
||||
|
||||
- **Models**
|
||||
- [CircleStone Anima 1.0](https://huggingface.co/circlestone-labs/Anima) in *Base* and *Turbo* (distilled) variants
|
||||
in both original precision and SDNQ-4bit quantiztion
|
||||
- [Microsoft Lens](https://huggingface.co/microsoft/Lens) in *Standard*, *Base* and *Turbo* (distilled) variant
|
||||
3.8B text-to-image DiT model with 12B GPT-OSS text-encoding and Flux2 VAE
|
||||
oh, that 12B encoder is MoE with 3.6B activated plus its prequantized using `mxfp4`
|
||||
*note* Lens comes with its own prompt-refiner, enable in settings -> model options (disabled by default)
|
||||
- **Features**
|
||||
- **SDNQ** new quantization algorithm: *Hadamard Rotations*
|
||||
much higher quality than base SDNQ, but runs slightly slower
|
||||
|
||||
@@ -28,6 +28,15 @@
|
||||
"tags": "distilled",
|
||||
"extras": "sampler: Default, cfg_scale: 7.0"
|
||||
},
|
||||
"Microsoft Lens Turbo": {
|
||||
"path": "microsoft/Lens-Turbo",
|
||||
"preview": "microsoft--Lens-Turbo.jpg",
|
||||
"desc": "Microsoft Lens-Turbo is the distilled Lens variant optimized for faster text-to-image generation with fewer steps.",
|
||||
"skip": true,
|
||||
"tags": "distilled",
|
||||
"size": 30.53,
|
||||
"date": "2026 May"
|
||||
},
|
||||
"Tencent FLUX.1 Dev SRPO": {
|
||||
"path": "vladmandic/flux.1-dev-SRPO",
|
||||
"preview": "vladmandic--flux.1-dev-SRPO.jpg",
|
||||
|
||||
@@ -38,6 +38,22 @@
|
||||
"size": 6.94,
|
||||
"date": "2023 July"
|
||||
},
|
||||
"Microsoft Lens": {
|
||||
"path": "microsoft/Lens",
|
||||
"preview": "microsoft--Lens.jpg",
|
||||
"desc": "Microsoft Lens is a text-to-image DiT model using GPT-OSS chat-style prompt encoding and Flux2 VAE decoding.",
|
||||
"skip": true,
|
||||
"size": 30.53,
|
||||
"date": "2026 May"
|
||||
},
|
||||
"Microsoft Lens Base": {
|
||||
"path": "microsoft/Lens-Base",
|
||||
"preview": "microsoft--Lens-Base.jpg",
|
||||
"desc": "Microsoft Lens-Base is the base variant of Lens for text-to-image generation with GPT-OSS prompt features.",
|
||||
"skip": true,
|
||||
"size": 30.53,
|
||||
"date": "2026 May"
|
||||
},
|
||||
"StabilityAI Stable Cascade": {
|
||||
"path": "huggingface/stabilityai/stable-cascade",
|
||||
"skip": true,
|
||||
|
||||
@@ -94,6 +94,8 @@ def get_model_type(pipe):
|
||||
model_type = 'kolors'
|
||||
elif 'Meissonic' in name:
|
||||
model_type = 'meissonic'
|
||||
elif 'LensPipeline' in name:
|
||||
model_type = 'lens'
|
||||
elif 'Qwen' in name:
|
||||
model_type = 'qwen'
|
||||
elif 'ErnieImage' in name or 'ERNIE-Image' in name:
|
||||
|
||||
@@ -99,6 +99,8 @@ def guess_by_name(fn, current_guess):
|
||||
new_guess = 'FLUX2 Klein'
|
||||
elif 'flux.2' in fn.lower():
|
||||
new_guess = 'FLUX2'
|
||||
elif 'lens' in fn.lower():
|
||||
new_guess = 'Lens'
|
||||
elif 'ultraflux' in fn.lower():
|
||||
new_guess = 'UltraFlux'
|
||||
elif 'flux' in fn.lower() or 'flex.1' in fn.lower():
|
||||
|
||||
@@ -486,6 +486,10 @@ def load_diffuser_force(detected_model_type, checkpoint_info, diffusers_load_con
|
||||
from pipelines.model_hunyuandit import load_hunyuandit
|
||||
sd_model = load_hunyuandit(checkpoint_info, diffusers_load_config)
|
||||
allow_post_quant = False
|
||||
elif model_type in ['Lens']:
|
||||
from pipelines.model_lens import load_lens
|
||||
sd_model = load_lens(checkpoint_info, diffusers_load_config)
|
||||
allow_post_quant = False
|
||||
elif model_type in ['Kandinsky 2.1']:
|
||||
from pipelines.model_kandinsky import load_kandinsky21
|
||||
sd_model = load_kandinsky21(checkpoint_info, diffusers_load_config)
|
||||
|
||||
@@ -54,6 +54,7 @@ pipelines = {
|
||||
'ERNIE-Image': getattr(diffusers, 'ErnieImagePipeline', None),
|
||||
'Nucleus-Image': getattr(diffusers, 'NucleusMoEImagePipeline', None),
|
||||
'Z-Image': getattr(diffusers, 'ZImagePipeline', None),
|
||||
'Lens': getattr(diffusers, 'LensPipeline', None),
|
||||
'FLUX2': getattr(diffusers, 'Flux2Pipeline', None),
|
||||
'FLUX2 Klein': getattr(diffusers, 'Flux2KleinPipeline', None),
|
||||
'LongCat': getattr(diffusers, 'LongCatImagePipeline', None),
|
||||
@@ -142,6 +143,12 @@ def get_pipelines():
|
||||
log.error(f'ONNX initialization error: {e}')
|
||||
onnx_pipelines = {}
|
||||
pipelines.update(onnx_pipelines)
|
||||
if 'Lens' in pipelines and pipelines['Lens'] is None:
|
||||
try:
|
||||
import pipelines.lens as _lens
|
||||
pipelines['Lens'] = getattr(diffusers, 'LensPipeline', None)
|
||||
except Exception:
|
||||
pass
|
||||
for k, v in pipelines.items():
|
||||
if k != 'Autodetect' and v is None:
|
||||
from modules.logger import log
|
||||
@@ -158,6 +165,8 @@ def get_repo(model):
|
||||
return 'stabilityai/stable-diffusion-3.5-medium'
|
||||
elif model == 'FluxPipeline' or model == 'FLUX':
|
||||
return 'black-forest-labs/FLUX.1-dev'
|
||||
elif model == 'LensPipeline' or model == 'Lens':
|
||||
return 'microsoft/Lens'
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@@ -113,6 +113,8 @@ def create_settings(cmd_opts):
|
||||
"model_qwen_layers": OptionInfo(2, "Qwen layered number of layers", gr.Slider, {"minimum": 2, "maximum": 9, "step": 1 }),
|
||||
"model_ernie_sep": OptionInfo("<h2>ERNIE-Image</h2>", "", gr.HTML),
|
||||
"model_ernie_enable_pe": OptionInfo(False, "Enable prompt-enhance"),
|
||||
"model_lens_sep": OptionInfo("<h2>Lens</h2>", "", gr.HTML),
|
||||
"model_lens_enable_pe": OptionInfo(False, "Enable prompt-enhance"),
|
||||
}))
|
||||
|
||||
# --- Model Offloading ---
|
||||
|
||||
@@ -71,7 +71,7 @@ def get_model(model_cls, variant=None):
|
||||
elif model_cls in {'f1', 'h1', 'zimage', 'lumina2', 'chroma', 'longcat', 'omnigen2', 'flite', 'ovis', 'kandinsky5', 'glmimage', 'cogview3', 'cogview4', 'ultraflux'}:
|
||||
model_cls = 'f1'
|
||||
variant = 'TAE FLUX.1'
|
||||
elif model_cls in {'f2', 'ernieimage'}:
|
||||
elif model_cls in {'f2', 'ernieimage', 'lens'}:
|
||||
model_cls = 'f2'
|
||||
variant = 'TAE FLUX.2'
|
||||
elif model_cls in {'sd3'}:
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Lens - minimal text-to-image inference package."""
|
||||
|
||||
import diffusers as _diffusers
|
||||
import transformers as _transformers
|
||||
from .pipeline import LensPipeline, LensPipelineOutput
|
||||
from .reasoner import PromptReasoner
|
||||
from .resolution import RESOLUTION_BUCKETS, resolve_resolution
|
||||
from .text_encoder import LensGptOssEncoder
|
||||
from .transformer import LensTransformer2DModel
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Make our custom subclasses discoverable by ``diffusers.DiffusionPipeline``.
|
||||
#
|
||||
# When ``LensPipeline.from_pretrained`` reads ``model_index.json``, it sees
|
||||
# entries like ``["transformers", "LensGptOssEncoder"]`` and runs
|
||||
# ``getattr(importlib.import_module("transformers"), "LensGptOssEncoder")``.
|
||||
# diffusers only allow-lists the libraries ``diffusers``, ``transformers`` and
|
||||
# ``onnxruntime.training`` - any other name is interpreted as a custom .py file
|
||||
# in the repo. So we inject our subclasses into those two namespaces here.
|
||||
#
|
||||
# Importing ``lens`` is required before calling ``LensPipeline.from_pretrained``
|
||||
# (this happens automatically when the user does ``from lens import LensPipeline``).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if not hasattr(_transformers, "LensGptOssEncoder"):
|
||||
_transformers.LensGptOssEncoder = LensGptOssEncoder
|
||||
if not hasattr(_diffusers, "LensTransformer2DModel"):
|
||||
_diffusers.LensTransformer2DModel = LensTransformer2DModel
|
||||
if not hasattr(_diffusers, "LensPipeline"):
|
||||
_diffusers.LensPipeline = LensPipeline
|
||||
|
||||
del _diffusers, _transformers
|
||||
|
||||
__all__ = [
|
||||
"LensPipeline",
|
||||
"LensPipelineOutput",
|
||||
"LensTransformer2DModel",
|
||||
"LensGptOssEncoder",
|
||||
"PromptReasoner",
|
||||
"RESOLUTION_BUCKETS",
|
||||
"resolve_resolution",
|
||||
]
|
||||
@@ -0,0 +1,647 @@
|
||||
"""Lens text-to-image pipeline.
|
||||
|
||||
The pipeline follows the standard ``diffusers`` component and call conventions:
|
||||
components are registered via ``register_modules`` and the call signature
|
||||
supports ``height``/``width``, ``generator``, ``prompt_embeds``, ``output_type``,
|
||||
``return_dict``, and ``callback_on_step_end``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Sequence, Union
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from diffusers import (
|
||||
AutoencoderKLFlux2,
|
||||
DiffusionPipeline,
|
||||
FlowMatchEulerDiscreteScheduler,
|
||||
)
|
||||
from diffusers.utils import BaseOutput
|
||||
from diffusers.utils.torch_utils import randn_tensor
|
||||
from einops import rearrange
|
||||
from PIL import Image
|
||||
|
||||
from .reasoner import PromptReasoner
|
||||
from .resolution import resolve_resolution
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from transformers import PreTrainedTokenizerBase
|
||||
from .text_encoder import LensGptOssEncoder
|
||||
from .transformer import LensTransformer2DModel
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def compute_empirical_mu(image_seq_len: int, num_steps: int) -> float:
|
||||
"""Empirical ``mu`` for ``FlowMatchEulerDiscreteScheduler`` dynamic shift.
|
||||
|
||||
Constants are calibrated for the Lens inference schedule.
|
||||
"""
|
||||
a1, b1 = 8.73809524e-05, 1.89833333
|
||||
a2, b2 = 0.00016927, 0.45666666
|
||||
if image_seq_len > 4300:
|
||||
return float(a2 * image_seq_len + b2)
|
||||
m_200 = a2 * image_seq_len + b2
|
||||
m_10 = a1 * image_seq_len + b1
|
||||
a = (m_200 - m_10) / 190.0
|
||||
b = m_200 - 200.0 * a
|
||||
return float(a * num_steps + b)
|
||||
|
||||
|
||||
# Chat template constants used by the Lens text encoder.
|
||||
_CHAT_SYSTEM = (
|
||||
"Describe the image by detailing the color, shape, size, texture, "
|
||||
"quantity, text, spatial relationships of the objects and background."
|
||||
)
|
||||
_CHAT_ASSISTANT_THINKING = "Need to generate one image according to the description."
|
||||
DEFAULT_TXT_OFFSET = 97
|
||||
|
||||
|
||||
# Default Lens transformer architecture.
|
||||
DEFAULT_TRANSFORMER_CONFIG = dict(
|
||||
patch_size=2,
|
||||
in_channels=128,
|
||||
out_channels=32,
|
||||
num_layers=48,
|
||||
attention_head_dim=64,
|
||||
num_attention_heads=24,
|
||||
inner_dim=1536,
|
||||
enc_hidden_dim=2880,
|
||||
axes_dims_rope=(8, 28, 28),
|
||||
gate_mlp=True,
|
||||
rms_norm=True,
|
||||
multi_layer_encoder_feature=True,
|
||||
selected_layer_index=(5, 11, 17, 23),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LensPipelineOutput(BaseOutput):
|
||||
"""Output of :class:`LensPipeline`.
|
||||
|
||||
Args:
|
||||
images: list of decoded PIL images, or a numpy array of shape
|
||||
``[B, H, W, C]`` when ``output_type='np'``, or the raw latent
|
||||
tensor when ``output_type='latent'``.
|
||||
"""
|
||||
|
||||
images: Union[List[Image.Image], np.ndarray, torch.Tensor]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class LensPipeline(DiffusionPipeline):
|
||||
r"""Lens text-to-image pipeline (GPT-OSS multi-layer features + Flux2 VAE).
|
||||
|
||||
Args:
|
||||
scheduler ([`FlowMatchEulerDiscreteScheduler`]):
|
||||
A scheduler used together with ``transformer`` to denoise the
|
||||
encoded image latents.
|
||||
vae ([`AutoencoderKLFlux2`]):
|
||||
Flux2 VAE used to decode latents into pixel images.
|
||||
text_encoder ([`LensGptOssEncoder`]):
|
||||
``GptOssForCausalLM`` subclass that exposes hidden states at the
|
||||
configured ``selected_layer_index`` via ``encode_layers(...)``.
|
||||
tokenizer ([`PreTrainedTokenizerBase`]):
|
||||
GPT-OSS tokenizer.
|
||||
transformer ([`LensTransformer2DModel`]):
|
||||
The Lens denoising DiT.
|
||||
reasoner ([`PromptReasoner`], *optional*):
|
||||
Optional prompt rewriter (local OSS ``generate`` or
|
||||
OpenAI-compatible API).
|
||||
"""
|
||||
|
||||
model_cpu_offload_seq = "text_encoder->transformer->vae"
|
||||
_callback_tensor_inputs = [
|
||||
"latents", "prompt_embeds", "negative_prompt_embeds",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scheduler: FlowMatchEulerDiscreteScheduler,
|
||||
vae: AutoencoderKLFlux2,
|
||||
text_encoder: LensGptOssEncoder,
|
||||
tokenizer: PreTrainedTokenizerBase,
|
||||
transformer: LensTransformer2DModel,
|
||||
reasoner: Optional[PromptReasoner] = True,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.register_modules(
|
||||
scheduler=scheduler,
|
||||
vae=vae,
|
||||
text_encoder=text_encoder,
|
||||
tokenizer=tokenizer,
|
||||
transformer=transformer,
|
||||
reasoner=reasoner,
|
||||
)
|
||||
if self.tokenizer.pad_token_id is None:
|
||||
self.tokenizer.pad_token = self.tokenizer.eos_token
|
||||
self.tokenizer.padding_side = "right"
|
||||
# Flux2 latent tile factor (4x4 patchify) and Lens DiT in_channels=128.
|
||||
self.vae_scale_factor = 16
|
||||
self.latent_channels = self.transformer.config.in_channels
|
||||
self.txt_offset = DEFAULT_TXT_OFFSET
|
||||
self.default_sample_size = 1024
|
||||
|
||||
if not hasattr(self.text_encoder, "_lens_selected_layers"):
|
||||
self.text_encoder.set_selected_layers(
|
||||
self.transformer.config.selected_layer_index
|
||||
)
|
||||
|
||||
if reasoner is not None:
|
||||
self.reasoner = PromptReasoner(
|
||||
text_encoder=self.text_encoder, tokenizer=self.tokenizer
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Prompt encoding
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_chat_inputs(
|
||||
self, prompts: Sequence[str], max_sequence_length: int, device: torch.device
|
||||
):
|
||||
rendered: List[str] = []
|
||||
for prompt in prompts:
|
||||
conversation = [
|
||||
{"role": "system", "content": _CHAT_SYSTEM, "thinking": None},
|
||||
{"role": "user", "content": prompt, "thinking": None},
|
||||
{"role": "assistant", "thinking": _CHAT_ASSISTANT_THINKING, "content": ""},
|
||||
]
|
||||
text = self.tokenizer.apply_chat_template(
|
||||
conversation, tokenize=False, add_generation_prompt=False
|
||||
)
|
||||
text = text.split("<|return|>")[0]
|
||||
rendered.append(text)
|
||||
|
||||
encoded = self.tokenizer(
|
||||
rendered,
|
||||
padding=True,
|
||||
truncation=True,
|
||||
max_length=max_sequence_length,
|
||||
return_tensors="pt",
|
||||
add_special_tokens=True,
|
||||
)
|
||||
return encoded["input_ids"].to(device), encoded["attention_mask"].to(device)
|
||||
|
||||
@torch.no_grad()
|
||||
def _get_text_embeddings(
|
||||
self, prompts: List[str], max_sequence_length: int, device: torch.device
|
||||
):
|
||||
input_ids, attn_mask = self._build_chat_inputs(prompts, max_sequence_length, device)
|
||||
layer_outputs = self.text_encoder.encode_layers(input_ids, attn_mask)
|
||||
|
||||
offset = self.txt_offset
|
||||
if input_ids.shape[1] > offset:
|
||||
features = [feat[:, offset:, :].contiguous() for feat in layer_outputs]
|
||||
mask = attn_mask[:, offset:].bool()
|
||||
else:
|
||||
zero_shape = (input_ids.shape[0], 0, layer_outputs[0].shape[-1])
|
||||
features = [layer_outputs[0].new_zeros(zero_shape) for _ in layer_outputs]
|
||||
mask = torch.zeros(
|
||||
(input_ids.shape[0], 0), dtype=torch.bool, device=device
|
||||
)
|
||||
return features, mask
|
||||
|
||||
def encode_prompt(
|
||||
self,
|
||||
prompt: Union[str, List[str]],
|
||||
negative_prompt: Union[str, List[str]] = "",
|
||||
num_images_per_prompt: int = 1,
|
||||
prompt_embeds: Optional[List[torch.Tensor]] = None,
|
||||
prompt_mask: Optional[torch.Tensor] = None,
|
||||
negative_prompt_embeds: Optional[List[torch.Tensor]] = None,
|
||||
negative_prompt_mask: Optional[torch.Tensor] = None,
|
||||
max_sequence_length: int = 512,
|
||||
device: Optional[torch.device] = None,
|
||||
):
|
||||
"""Encode positives and negatives. Returns
|
||||
``(prompt_embeds, prompt_mask, negative_prompt_embeds, negative_prompt_mask)``
|
||||
where each ``*_embeds`` is a list of per-layer tensors and each
|
||||
``*_mask`` is a bool ``[B*N, S]`` tensor.
|
||||
|
||||
Each unique prompt is encoded **once**; the resulting features and mask
|
||||
are then ``repeat_interleave``-d ``num_images_per_prompt`` times along
|
||||
the batch axis. This preserves the ``[p0,p0,...,p1,p1,...]`` ordering
|
||||
downstream consumers expect.
|
||||
"""
|
||||
device = device or self._execution_device
|
||||
|
||||
prompts = [prompt] if isinstance(prompt, str) else list(prompt)
|
||||
n = int(num_images_per_prompt)
|
||||
|
||||
# Negatives broadcast.
|
||||
if isinstance(negative_prompt, str):
|
||||
negatives = [negative_prompt] * len(prompts)
|
||||
else:
|
||||
negatives = list(negative_prompt)
|
||||
if len(negatives) == 1:
|
||||
negatives = negatives * len(prompts)
|
||||
if len(negatives) != len(prompts):
|
||||
raise ValueError(
|
||||
"negative_prompt must be a string or a list of the same "
|
||||
"length as prompt"
|
||||
)
|
||||
|
||||
if prompt_embeds is None:
|
||||
prompt_embeds, prompt_mask = self._get_text_embeddings(
|
||||
prompts, max_sequence_length, device
|
||||
)
|
||||
prompt_embeds, prompt_mask = self._repeat_for_n(prompt_embeds, prompt_mask, n)
|
||||
elif prompt_mask is None:
|
||||
raise ValueError("`prompt_mask` must be provided when passing `prompt_embeds`.")
|
||||
if negative_prompt_embeds is None:
|
||||
if all(isinstance(neg, str) and not neg.strip() for neg in negatives):
|
||||
# Empty negatives use an unconditional branch with no text tokens.
|
||||
negative_prompt_embeds = [
|
||||
feat.new_zeros(feat.shape) for feat in prompt_embeds
|
||||
]
|
||||
negative_prompt_mask = torch.zeros_like(prompt_mask, dtype=torch.bool)
|
||||
else:
|
||||
negative_prompt_embeds, negative_prompt_mask = self._get_text_embeddings(
|
||||
negatives, max_sequence_length, device
|
||||
)
|
||||
negative_prompt_embeds, negative_prompt_mask = self._repeat_for_n(
|
||||
negative_prompt_embeds, negative_prompt_mask, n
|
||||
)
|
||||
elif negative_prompt_mask is None:
|
||||
raise ValueError(
|
||||
"`negative_prompt_mask` must be provided when passing "
|
||||
"`negative_prompt_embeds`."
|
||||
)
|
||||
return prompt_embeds, prompt_mask, negative_prompt_embeds, negative_prompt_mask
|
||||
|
||||
@staticmethod
|
||||
def _repeat_for_n(features: List[torch.Tensor], mask: torch.Tensor, n: int):
|
||||
"""Repeat each sample ``n`` times along the batch axis (interleaved)."""
|
||||
if n == 1:
|
||||
return features, mask
|
||||
features = [f.repeat_interleave(n, dim=0) for f in features]
|
||||
mask = mask.repeat_interleave(n, dim=0)
|
||||
return features, mask
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reasoner shim
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def refine_prompt(
|
||||
self, prompts: Sequence[str], enable_reasoner: bool = False
|
||||
) -> List[str]:
|
||||
if self.reasoner is None:
|
||||
return list(prompts)
|
||||
print('HERE REFINE')
|
||||
return self.reasoner.refine(prompts, enable=enable_reasoner)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Latent prep
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def prepare_latents(
|
||||
self,
|
||||
batch_size: int,
|
||||
num_channels_latents: int,
|
||||
height: int,
|
||||
width: int,
|
||||
dtype: torch.dtype,
|
||||
device: torch.device,
|
||||
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
|
||||
latents: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
latent_h = height // self.vae_scale_factor
|
||||
latent_w = width // self.vae_scale_factor
|
||||
shape = (batch_size, latent_h * latent_w, num_channels_latents)
|
||||
if latents is not None:
|
||||
return latents.to(device=device, dtype=dtype)
|
||||
return randn_tensor(shape, generator=generator, device=device, dtype=dtype)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Input checks
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def check_inputs(
|
||||
self,
|
||||
prompt,
|
||||
height,
|
||||
width,
|
||||
prompt_embeds,
|
||||
callback_on_step_end_tensor_inputs,
|
||||
) -> None:
|
||||
if height is None or width is None:
|
||||
raise ValueError(
|
||||
"height and width must be provided (or use base_resolution + aspect_ratio)."
|
||||
)
|
||||
if height % self.vae_scale_factor or width % self.vae_scale_factor:
|
||||
raise ValueError(
|
||||
f"height and width must be divisible by {self.vae_scale_factor}; "
|
||||
f"got ({height}, {width})."
|
||||
)
|
||||
if prompt is None and prompt_embeds is None:
|
||||
raise ValueError("Either `prompt` or `prompt_embeds` must be provided.")
|
||||
if callback_on_step_end_tensor_inputs is not None:
|
||||
for k in callback_on_step_end_tensor_inputs:
|
||||
if k not in self._callback_tensor_inputs:
|
||||
raise ValueError(
|
||||
f"callback_on_step_end_tensor_inputs entry {k!r} is not "
|
||||
f"in {self._callback_tensor_inputs}."
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Decode
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _patchify_latents(latents: torch.Tensor) -> torch.Tensor:
|
||||
b, c, h, w = latents.shape
|
||||
latents = latents.view(b, c, h // 2, 2, w // 2, 2)
|
||||
latents = latents.permute(0, 1, 3, 5, 2, 4)
|
||||
return latents.reshape(b, c * 4, h // 2, w // 2)
|
||||
|
||||
@staticmethod
|
||||
def _unpatchify_latents(latents: torch.Tensor) -> torch.Tensor:
|
||||
b, c, h, w = latents.shape
|
||||
latents = latents.reshape(b, c // 4, 2, 2, h, w)
|
||||
latents = latents.permute(0, 1, 4, 2, 5, 3)
|
||||
return latents.reshape(b, c // 4, h * 2, w * 2)
|
||||
|
||||
@torch.no_grad()
|
||||
def _decode(self, latents: torch.Tensor, latent_h: int, latent_w: int):
|
||||
latents = rearrange(
|
||||
latents,
|
||||
"b (h w) (c p1 p2) -> b c (h p1) (w p2)",
|
||||
p1=2, p2=2, h=latent_h, w=latent_w,
|
||||
)
|
||||
latents = latents.to(self.vae.dtype)
|
||||
# Reverse the VAE latent normalization used by Lens. We compute the
|
||||
# shift/scale at runtime from the live ``vae.bn`` so this stays correct
|
||||
# under cpu-offload (where the VAE may be moved between devices).
|
||||
bn = self.vae.bn
|
||||
mean = bn.running_mean.view(1, -1, 1, 1)
|
||||
var = bn.running_var.view(1, -1, 1, 1)
|
||||
std = torch.sqrt(var + self.vae.config.batch_norm_eps)
|
||||
shift = (-mean).to(device=latents.device, dtype=latents.dtype)
|
||||
scale = (1.0 / std).to(device=latents.device, dtype=latents.dtype)
|
||||
x = self._patchify_latents(latents)
|
||||
x = x / scale - shift
|
||||
x = self._unpatchify_latents(x)
|
||||
return self.vae.decode(x).sample
|
||||
|
||||
@staticmethod
|
||||
def _to_pil(image: torch.Tensor) -> List[Image.Image]:
|
||||
# image: [B, C, H, W] in [-1, 1].
|
||||
image = image.clamp(-1.0, 1.0)
|
||||
image = (image + 1.0) * (255.0 / 2.0)
|
||||
image = image.permute(0, 2, 3, 1).to(device="cpu", dtype=torch.uint8).numpy()
|
||||
return [Image.fromarray(im) for im in image]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# __call__
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@torch.no_grad()
|
||||
def __call__(
|
||||
self,
|
||||
prompt: Union[str, List[str]] = None, # noqa: RUF013
|
||||
negative_prompt: Union[str, List[str]] = "",
|
||||
height: Optional[int] = None,
|
||||
width: Optional[int] = None,
|
||||
base_resolution: Optional[int] = None,
|
||||
aspect_ratio: Optional[str] = None,
|
||||
num_inference_steps: int = 50,
|
||||
guidance_scale: float = 4.0,
|
||||
num_images_per_prompt: int = 1,
|
||||
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
|
||||
latents: Optional[torch.Tensor] = None,
|
||||
prompt_embeds: Optional[List[torch.Tensor]] = None,
|
||||
prompt_mask: Optional[torch.Tensor] = None,
|
||||
negative_prompt_embeds: Optional[List[torch.Tensor]] = None,
|
||||
negative_prompt_mask: Optional[torch.Tensor] = None,
|
||||
output_type: str = "pil",
|
||||
return_dict: bool = True,
|
||||
callback_on_step_end: Optional[Callable[[Any, int, int, Dict], Dict]] = None,
|
||||
callback_on_step_end_tensor_inputs: List[str] = ["latents"],
|
||||
max_sequence_length: int = 512,
|
||||
enable_reasoner: bool = False,
|
||||
):
|
||||
# 0. Resolution defaulting.
|
||||
if base_resolution is not None and aspect_ratio is not None:
|
||||
height, width = resolve_resolution(base_resolution, aspect_ratio)
|
||||
elif height is None or width is None:
|
||||
height = width = self.default_sample_size
|
||||
|
||||
# 1. Input validation.
|
||||
self.check_inputs(
|
||||
prompt, height, width, prompt_embeds, callback_on_step_end_tensor_inputs
|
||||
)
|
||||
|
||||
device = self._execution_device
|
||||
dtype = self.transformer.dtype
|
||||
|
||||
# 2. Reasoner refinement (no-op when disabled and no API).
|
||||
if prompt is not None:
|
||||
prompts = [prompt] if isinstance(prompt, str) else list(prompt)
|
||||
prompts = self.refine_prompt(prompts, enable_reasoner=enable_reasoner)
|
||||
self._last_refined_prompts = prompts # pylint: disable=attribute-defined-outside-init
|
||||
else:
|
||||
prompts = None
|
||||
|
||||
# 3. Encode positives and negatives.
|
||||
prompt_embeds, prompt_mask, negative_prompt_embeds, negative_prompt_mask = self.encode_prompt(
|
||||
prompt=prompts,
|
||||
negative_prompt=negative_prompt,
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
prompt_embeds=prompt_embeds,
|
||||
prompt_mask=prompt_mask,
|
||||
negative_prompt_embeds=negative_prompt_embeds,
|
||||
negative_prompt_mask=negative_prompt_mask,
|
||||
max_sequence_length=max_sequence_length,
|
||||
device=device,
|
||||
)
|
||||
|
||||
# 4. Pad pos/neg to a shared S_txt for joint CFG batching.
|
||||
prompt_embeds, prompt_mask, negative_prompt_embeds, negative_prompt_mask = self._align_text_features(
|
||||
prompt_embeds, prompt_mask, negative_prompt_embeds, negative_prompt_mask
|
||||
)
|
||||
|
||||
encoder_features = [
|
||||
torch.cat([pf, nf], dim=0).to(dtype=dtype)
|
||||
for pf, nf in zip(prompt_embeds, negative_prompt_embeds)
|
||||
]
|
||||
encoder_mask = torch.cat([prompt_mask, negative_prompt_mask], dim=0)
|
||||
|
||||
# 5. Prepare latents.
|
||||
batch_size = prompt_embeds[0].shape[0]
|
||||
latent_h = height // self.vae_scale_factor
|
||||
latent_w = width // self.vae_scale_factor
|
||||
seq_len = latent_h * latent_w
|
||||
latents = self.prepare_latents(
|
||||
batch_size, self.latent_channels, height, width,
|
||||
dtype=dtype, device=device, generator=generator, latents=latents,
|
||||
)
|
||||
|
||||
# 6. Scheduler.
|
||||
mu = compute_empirical_mu(seq_len, num_inference_steps)
|
||||
sigmas = np.linspace(1.0, 1.0 / num_inference_steps, num_inference_steps)
|
||||
self.scheduler.set_timesteps(sigmas=sigmas, device=device, mu=mu)
|
||||
|
||||
# 7. Denoising loop.
|
||||
img_shapes = [(1, latent_h, latent_w)]
|
||||
with self.progress_bar(total=num_inference_steps) as progress_bar:
|
||||
for i, t in enumerate(self.scheduler.timesteps):
|
||||
timestep = t.expand(batch_size * 2).to(latents.dtype)
|
||||
hidden_states = latents.repeat(2, 1, 1)
|
||||
|
||||
noise = self.transformer(
|
||||
hidden_states=hidden_states,
|
||||
encoder_hidden_states=encoder_features,
|
||||
encoder_hidden_states_mask=encoder_mask,
|
||||
timestep=timestep / 1000,
|
||||
img_shapes=img_shapes,
|
||||
)
|
||||
|
||||
cond, uncond = noise.chunk(2)
|
||||
comb = uncond + guidance_scale * (cond - uncond)
|
||||
cond_norm = torch.norm(cond, dim=-1, keepdim=True)
|
||||
comb_norm = torch.norm(comb, dim=-1, keepdim=True)
|
||||
scale = torch.where(
|
||||
comb_norm > 0,
|
||||
cond_norm / comb_norm.clamp_min(1e-12),
|
||||
torch.ones_like(comb_norm),
|
||||
)
|
||||
noise_pred = comb * scale
|
||||
|
||||
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
|
||||
|
||||
if callback_on_step_end is not None:
|
||||
cb_kwargs = {
|
||||
k: locals()[k] for k in callback_on_step_end_tensor_inputs
|
||||
}
|
||||
cb_out = callback_on_step_end(self, i, t, cb_kwargs)
|
||||
latents = cb_out.pop("latents", latents)
|
||||
prompt_embeds = cb_out.pop("prompt_embeds", prompt_embeds)
|
||||
negative_prompt_embeds = cb_out.pop(
|
||||
"negative_prompt_embeds", negative_prompt_embeds
|
||||
)
|
||||
|
||||
progress_bar.update()
|
||||
|
||||
# 8. Decode.
|
||||
if output_type == "latent":
|
||||
images: Any = latents
|
||||
else:
|
||||
decoded = self._decode(latents, latent_h, latent_w)
|
||||
if output_type == "pil":
|
||||
images = self._to_pil(decoded)
|
||||
elif output_type == "np":
|
||||
decoded = decoded.clamp(-1.0, 1.0)
|
||||
decoded = (decoded + 1.0) * 0.5
|
||||
images = decoded.permute(0, 2, 3, 1).to("cpu", torch.float32).numpy()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"output_type must be one of 'pil', 'np', 'latent'; got {output_type!r}."
|
||||
)
|
||||
|
||||
self.maybe_free_model_hooks()
|
||||
|
||||
if not return_dict:
|
||||
return (images,)
|
||||
return LensPipelineOutput(images=images)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Misc helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _align_text_features(
|
||||
pos_features: List[torch.Tensor],
|
||||
pos_mask: torch.Tensor,
|
||||
neg_features: List[torch.Tensor],
|
||||
neg_mask: torch.Tensor,
|
||||
):
|
||||
"""Pad pos/neg encodings and masks to a common ``S_txt``."""
|
||||
if not pos_features or not neg_features:
|
||||
raise ValueError("Positive and negative text feature lists must be non-empty.")
|
||||
if len(pos_features) != len(neg_features):
|
||||
raise ValueError(
|
||||
"Positive and negative text feature lists must have the same "
|
||||
f"number of layers; got {len(pos_features)} and {len(neg_features)}."
|
||||
)
|
||||
seq_pos = pos_features[0].shape[1]
|
||||
seq_neg = neg_features[0].shape[1]
|
||||
if pos_mask.shape[1] != seq_pos:
|
||||
raise ValueError(
|
||||
f"prompt_mask length {pos_mask.shape[1]} does not match "
|
||||
f"prompt feature length {seq_pos}."
|
||||
)
|
||||
if pos_mask.shape[0] != pos_features[0].shape[0]:
|
||||
raise ValueError(
|
||||
f"prompt_mask batch size {pos_mask.shape[0]} does not match "
|
||||
f"prompt feature batch size {pos_features[0].shape[0]}."
|
||||
)
|
||||
if neg_mask.shape[1] != seq_neg:
|
||||
raise ValueError(
|
||||
f"negative_prompt_mask length {neg_mask.shape[1]} does not "
|
||||
f"match negative prompt feature length {seq_neg}."
|
||||
)
|
||||
if neg_mask.shape[0] != neg_features[0].shape[0]:
|
||||
raise ValueError(
|
||||
f"negative_prompt_mask batch size {neg_mask.shape[0]} does "
|
||||
f"not match negative prompt feature batch size {neg_features[0].shape[0]}."
|
||||
)
|
||||
if pos_features[0].shape[0] != neg_features[0].shape[0]:
|
||||
raise ValueError(
|
||||
"Positive and negative text features must have the same batch "
|
||||
f"size; got {pos_features[0].shape[0]} and {neg_features[0].shape[0]}."
|
||||
)
|
||||
for i, feat in enumerate(pos_features):
|
||||
if feat.shape[:2] != pos_features[0].shape[:2]:
|
||||
raise ValueError(
|
||||
f"Positive feature layer {i} shape {feat.shape[:2]} does "
|
||||
f"not match layer 0 shape {pos_features[0].shape[:2]}."
|
||||
)
|
||||
for i, feat in enumerate(neg_features):
|
||||
if feat.shape[:2] != neg_features[0].shape[:2]:
|
||||
raise ValueError(
|
||||
f"Negative feature layer {i} shape {feat.shape[:2]} does "
|
||||
f"not match layer 0 shape {neg_features[0].shape[:2]}."
|
||||
)
|
||||
|
||||
target = max(seq_pos, seq_neg)
|
||||
|
||||
def pad(features: List[torch.Tensor], cur: int) -> List[torch.Tensor]:
|
||||
if cur == target:
|
||||
return features
|
||||
pad_len = target - cur
|
||||
return [
|
||||
torch.cat(
|
||||
[feat, feat.new_zeros((feat.shape[0], pad_len, feat.shape[-1]))],
|
||||
dim=1,
|
||||
)
|
||||
for feat in features
|
||||
]
|
||||
|
||||
def pad_mask(mask: torch.Tensor, cur: int) -> torch.Tensor:
|
||||
if cur == target:
|
||||
return mask
|
||||
return torch.cat(
|
||||
[
|
||||
mask,
|
||||
torch.zeros(
|
||||
(mask.shape[0], target - cur),
|
||||
dtype=torch.bool, device=mask.device,
|
||||
),
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
pos_features = pad(pos_features, seq_pos)
|
||||
neg_features = pad(neg_features, seq_neg)
|
||||
pos_mask = pad_mask(pos_mask.bool(), seq_pos)
|
||||
neg_mask = pad_mask(neg_mask.bool(), seq_neg)
|
||||
return pos_features, pos_mask, neg_features, neg_mask
|
||||
@@ -0,0 +1,252 @@
|
||||
"""Prompt reasoner - refines user prompts before they hit the text encoder.
|
||||
|
||||
Decision matrix (driven by ``enable`` and whether an OpenAI-compatible API
|
||||
is configured):
|
||||
|
||||
| ``enable`` | OpenAI API set? | Behavior |
|
||||
| ----------------- | --------------- | ----------------------------------------- |
|
||||
| ``False`` (default) | no | identity (return prompts unchanged) |
|
||||
| ``False`` | yes | refine via OpenAI-compatible API |
|
||||
| ``True`` | no | refine via the local GPT-OSS |
|
||||
| ``True`` | yes | refine via OpenAI-compatible API |
|
||||
|
||||
The OpenAI path uses any chat-completion endpoint speaking the OpenAI v1
|
||||
schema (e.g. ``vllm``, ``ollama --openai-compat``, ``together.ai``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
THINK_BLOCK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
|
||||
HARMONY_FINAL_RE = re.compile(
|
||||
r"<\|start\|>assistant(?:<\|channel\|>final)?<\|message\|>(.*?)(?:<\|return\|>|<\|end\|>|$)",
|
||||
re.DOTALL,
|
||||
)
|
||||
HARMONY_DIRECT_FINAL_RE = re.compile(
|
||||
r"<\|channel\|>final<\|message\|>(.*?)(?:<\|return\|>|<\|end\|>|$)",
|
||||
re.DOTALL,
|
||||
)
|
||||
PLAIN_HARMONY_FINAL_MARKER_RE = re.compile(r"assistant\s*final\s*", re.IGNORECASE)
|
||||
PLAIN_HARMONY_DIRECT_FINAL_RE = re.compile(r"(?:^|\n)\s*final\s*", re.IGNORECASE)
|
||||
|
||||
|
||||
SYSTEM_PROMPT = """
|
||||
You are a prompt rewriter for a text-to-image model.
|
||||
Your task is to convert the user's input into a single, precise, descriptive image prompt suitable for a text-to-image model.
|
||||
Follow these rules strictly:
|
||||
|
||||
1. The output must be a clear and accurate description of a single image scene, written in the style of a text-to-image prompt.
|
||||
- Do not include explanations, reasoning, commentary, or meta text.
|
||||
- Do not ask questions.
|
||||
- Do not output multiple options.
|
||||
- Do not use uncertain, speculative, or alternative wording such as "maybe", "possibly", "perhaps", "or", "might", or "could".
|
||||
|
||||
2. Preserve the user's intended scene faithfully.
|
||||
- Do not change the objects, entities, attributes, actions, relationships, or core setting explicitly described by the user.
|
||||
- You may add reasonable visual details only when they help make the image concrete and coherent.
|
||||
- Any added details must be consistent with the user's description and must not introduce new important objects or alter the meaning.
|
||||
|
||||
3. If the image contains many main subjects of the same kind, describe each subject in detail, including humans, animals, objects, and any other prominent elements.
|
||||
- For each subject, include its appearance, color, size, shape, material, pose, expression, and position if applicable in the scene.
|
||||
- Make sure every main subject is clearly distinguishable from the others, such as in a scene with "4 dogs," describing each dog separately.
|
||||
|
||||
4. The output must fully cover the scene implied by the user's input.
|
||||
- Include the main subjects, relevant attributes, actions, spatial relationships, environment, and visible details necessary to render the scene.
|
||||
- If the user input is already sufficiently detailed and already suitable for image generation, keep it unchanged or only make minimal edits for fluency and clarity.
|
||||
|
||||
5. Resolve content that requires simple inference into explicit visual results when the result is unambiguous and visually representable.
|
||||
- Example: if the user says "the answer to 2+2 is written on the blackboard", output should explicitly describe "the blackboard shows 2+2=4".
|
||||
- Use only direct, necessary inference that is clearly implied by the user input.
|
||||
- Do not invent hidden facts, backstory, or ambiguous details.
|
||||
|
||||
6. Language rule:
|
||||
- If the user input is not in English, output in the same language.
|
||||
- Otherwise, output in English.
|
||||
|
||||
7. Output format:
|
||||
- Output exactly one final rewritten prompt.
|
||||
- Do not use bullet points, numbering, JSON, XML, Markdown, or quotation marks unless they are part of the scene itself.
|
||||
|
||||
Your goal is to produce a prompt that is concrete, visual, faithful to the user intent, and directly usable as input to a text-to-image model.
|
||||
""".strip()
|
||||
|
||||
|
||||
def _extract_plain_harmony_final(text: str) -> Optional[str]:
|
||||
matches = list(PLAIN_HARMONY_FINAL_MARKER_RE.finditer(text))
|
||||
if matches:
|
||||
final_text = text[matches[-1].end() :].strip()
|
||||
return final_text or None
|
||||
|
||||
if text.lstrip().lower().startswith("analysis"):
|
||||
matches = list(PLAIN_HARMONY_DIRECT_FINAL_RE.finditer(text))
|
||||
if matches:
|
||||
final_text = text[matches[-1].end() :].strip()
|
||||
return final_text or None
|
||||
return None
|
||||
|
||||
|
||||
def _clean_reasoner_output(text: str) -> str:
|
||||
text = text.strip()
|
||||
final_match = None
|
||||
for match in HARMONY_FINAL_RE.finditer(text):
|
||||
final_match = match
|
||||
if final_match is not None:
|
||||
text = final_match.group(1).strip()
|
||||
else:
|
||||
direct_final_match = None
|
||||
for match in HARMONY_DIRECT_FINAL_RE.finditer(text):
|
||||
direct_final_match = match
|
||||
if direct_final_match is not None:
|
||||
text = direct_final_match.group(1).strip()
|
||||
else:
|
||||
plain_final = _extract_plain_harmony_final(text)
|
||||
if plain_final is not None:
|
||||
text = plain_final
|
||||
|
||||
text = THINK_BLOCK_RE.sub("", text).strip()
|
||||
if "</think>" in text.lower():
|
||||
text = re.split(r"</think>", text, flags=re.IGNORECASE)[-1].strip()
|
||||
plain_final = _extract_plain_harmony_final(text)
|
||||
if plain_final is not None:
|
||||
text = plain_final
|
||||
for token in (
|
||||
"<|channel|>analysis<|message|>",
|
||||
"<|start|>assistant<|channel|>analysis<|message|>",
|
||||
"<|channel|>final<|message|>",
|
||||
"<|start|>assistant<|channel|>final<|message|>",
|
||||
"<|start|>assistant<|message|>",
|
||||
"<|return|>",
|
||||
"<|end|>",
|
||||
"<|endoftext|>",
|
||||
"<|im_end|>",
|
||||
):
|
||||
text = text.replace(token, "")
|
||||
|
||||
text = text.strip()
|
||||
if re.match(r"^(?:analysis|assistant\s*analysis)(?:\b|[A-Z])", text, flags=re.IGNORECASE | re.DOTALL):
|
||||
return ""
|
||||
if text.startswith("```") and text.endswith("```"):
|
||||
lines = text.splitlines()
|
||||
if len(lines) >= 3:
|
||||
text = "\n".join(lines[1:-1]).strip()
|
||||
if len(text) >= 2 and text[0] == text[-1] == '"':
|
||||
text = text[1:-1].strip()
|
||||
return " ".join(text.split())
|
||||
|
||||
|
||||
class PromptReasoner:
|
||||
"""Optional prompt rewriter, used by ``LensPipeline.refine_prompt``."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
text_encoder=None,
|
||||
tokenizer=None,
|
||||
openai_api_key: Optional[str] = None,
|
||||
openai_base_url: Optional[str] = None,
|
||||
openai_model: Optional[str] = None,
|
||||
max_new_tokens: int = 4096,
|
||||
temperature: float = 0.7,
|
||||
) -> None:
|
||||
self.text_encoder = text_encoder
|
||||
self.tokenizer = tokenizer
|
||||
self.openai_api_key = openai_api_key
|
||||
self.openai_base_url = openai_base_url
|
||||
self.openai_model = openai_model
|
||||
self.max_new_tokens = int(max_new_tokens)
|
||||
self.temperature = float(temperature)
|
||||
self._client = None # lazily constructed
|
||||
|
||||
@property
|
||||
def has_api(self) -> bool:
|
||||
return bool(self.openai_api_key and self.openai_model)
|
||||
|
||||
def refine(self, prompts: Sequence[str], enable: bool) -> List[str]:
|
||||
prompts = list(prompts)
|
||||
# API takes precedence whenever it is configured.
|
||||
if self.has_api:
|
||||
return self._refine_via_api(prompts)
|
||||
if enable:
|
||||
if self.text_encoder is None or self.tokenizer is None:
|
||||
raise RuntimeError(
|
||||
"Reasoner enabled with no API: both text_encoder and "
|
||||
"tokenizer must be provided to use the local GPT-OSS as "
|
||||
"the reasoner."
|
||||
)
|
||||
return self._refine_via_local(prompts)
|
||||
return prompts
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Local GPT-OSS path
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@torch.no_grad()
|
||||
def _refine_via_local(self, prompts: List[str]) -> List[str]:
|
||||
refined: List[str] = []
|
||||
for prompt in prompts:
|
||||
system_prompt = (
|
||||
f"{SYSTEM_PROMPT}\n\n"
|
||||
"Keep any reasoning private. The visible answer must contain only the final rewritten prompt."
|
||||
)
|
||||
conversation = [
|
||||
{"role": "system", "content": system_prompt, "thinking": None},
|
||||
{"role": "user", "content": prompt, "thinking": None},
|
||||
]
|
||||
text = self.tokenizer.apply_chat_template(
|
||||
conversation, tokenize=False, add_generation_prompt=True, reasoning_effort="low"
|
||||
)
|
||||
input_ids = self.tokenizer(
|
||||
text, return_tensors="pt", add_special_tokens=True
|
||||
).input_ids
|
||||
out_ids = self.text_encoder.generate(
|
||||
input_ids,
|
||||
max_new_tokens=self.max_new_tokens,
|
||||
do_sample=self.temperature > 0.0,
|
||||
temperature=max(self.temperature, 1e-5),
|
||||
pad_token_id=self.tokenizer.pad_token_id,
|
||||
)
|
||||
new_tokens = out_ids[0, input_ids.shape[1]:]
|
||||
text_out = self.tokenizer.decode(new_tokens, skip_special_tokens=False)
|
||||
clean_text_out = _clean_reasoner_output(text_out)
|
||||
refined.append(clean_text_out or prompt)
|
||||
return refined
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# OpenAI-compatible API path
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _client_or_raise(self):
|
||||
if self._client is None:
|
||||
try:
|
||||
from openai import OpenAI
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"openai package not installed. `pip install openai` to use "
|
||||
"the API-based reasoner."
|
||||
) from exc
|
||||
self._client = OpenAI(
|
||||
api_key=self.openai_api_key,
|
||||
base_url=self.openai_base_url,
|
||||
)
|
||||
return self._client
|
||||
|
||||
def _refine_via_api(self, prompts: List[str]) -> List[str]:
|
||||
client = self._client_or_raise()
|
||||
out: List[str] = []
|
||||
for prompt in prompts:
|
||||
resp = client.chat.completions.create(
|
||||
model=self.openai_model,
|
||||
messages=[
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
max_tokens=self.max_new_tokens,
|
||||
)
|
||||
text = (resp.choices[0].message.content or "").strip()
|
||||
out.append(text or prompt)
|
||||
return out
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Resolution buckets for Lens inference.
|
||||
|
||||
Two base resolutions (1024 and 1440) crossed with nine aspect ratios. All
|
||||
heights/widths are divisible by 16 so they tile cleanly into Flux2 VAE latents
|
||||
(downsample factor 16).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Tuple
|
||||
|
||||
|
||||
# Bucket maps. Keys are aspect ratios as "W:H" strings; values are (height, width).
|
||||
# 1440 base buckets are computed as round_to_16(1024_value * 1440 / 1024).
|
||||
RESOLUTION_BUCKETS: Dict[int, Dict[str, Tuple[int, int]]] = {
|
||||
1024: {
|
||||
"1:2": (1472, 736),
|
||||
"9:16": (1376, 768),
|
||||
"2:3": (1248, 832),
|
||||
"3:4": (1152, 864),
|
||||
"1:1": (1024, 1024),
|
||||
"4:3": ( 864, 1152),
|
||||
"3:2": ( 832, 1248),
|
||||
"16:9": ( 768, 1376),
|
||||
"2:1": ( 736, 1472),
|
||||
},
|
||||
1440: {
|
||||
"1:2": (2080, 1040),
|
||||
"9:16": (1936, 1088),
|
||||
"2:3": (1760, 1168),
|
||||
"3:4": (1616, 1216),
|
||||
"1:1": (1440, 1440),
|
||||
"4:3": (1216, 1616),
|
||||
"3:2": (1168, 1760),
|
||||
"16:9": (1088, 1936),
|
||||
"2:1": (1040, 2080),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
SUPPORTED_BASE_RESOLUTIONS = tuple(RESOLUTION_BUCKETS.keys())
|
||||
SUPPORTED_ASPECT_RATIOS = tuple(RESOLUTION_BUCKETS[1024].keys())
|
||||
|
||||
|
||||
def resolve_resolution(base_resolution: int, aspect_ratio: str) -> Tuple[int, int]:
|
||||
"""Return (height, width) for the requested bucket.
|
||||
|
||||
Aspect ratio is interpreted as W:H (e.g. "16:9" is landscape,
|
||||
"9:16" is portrait).
|
||||
"""
|
||||
if base_resolution not in RESOLUTION_BUCKETS:
|
||||
raise ValueError(
|
||||
f"Unsupported base_resolution={base_resolution}. "
|
||||
f"Supported: {SUPPORTED_BASE_RESOLUTIONS}"
|
||||
)
|
||||
table = RESOLUTION_BUCKETS[base_resolution]
|
||||
if aspect_ratio not in table:
|
||||
raise ValueError(
|
||||
f"Unsupported aspect_ratio={aspect_ratio!r}. "
|
||||
f"Supported: {SUPPORTED_ASPECT_RATIOS}"
|
||||
)
|
||||
return table[aspect_ratio]
|
||||
@@ -0,0 +1,137 @@
|
||||
"""GPT-OSS text encoder for Lens.
|
||||
|
||||
We subclass ``transformers.GptOssForCausalLM`` so we can:
|
||||
|
||||
1. Return hidden states *only* at a configured layer subset (default
|
||||
``[5, 11, 17, 23]``), avoiding the memory cost of HF's stock
|
||||
``output_hidden_states=True`` which materializes every layer.
|
||||
2. Early-exit after the last selected layer, since we don't need the
|
||||
downstream LM head at all when extracting features.
|
||||
|
||||
Standard ``generate(...)`` is inherited unchanged and is used by the optional
|
||||
prompt reasoner.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
import torch
|
||||
from transformers.masking_utils import (
|
||||
create_causal_mask,
|
||||
create_sliding_window_causal_mask,
|
||||
)
|
||||
from transformers.models.gpt_oss.modeling_gpt_oss import GptOssForCausalLM
|
||||
|
||||
|
||||
class LensGptOssEncoder(GptOssForCausalLM):
|
||||
"""``GptOssForCausalLM`` subclass that exposes selected hidden states."""
|
||||
|
||||
def set_selected_layers(self, layer_indices: Sequence[int]) -> None:
|
||||
layers = [int(i) for i in layer_indices]
|
||||
if not layers:
|
||||
raise ValueError("layer_indices must be non-empty")
|
||||
if len(set(layers)) != len(layers):
|
||||
raise ValueError(f"layer_indices must be unique; got {layers}")
|
||||
if min(layers) < 0 or max(layers) >= len(self.model.layers):
|
||||
raise ValueError(
|
||||
f"layer_indices out of range; got {layers}, "
|
||||
f"model has {len(self.model.layers)} layers"
|
||||
)
|
||||
self._lens_selected_layers = layers
|
||||
self._lens_max_layer = max(layers)
|
||||
|
||||
@torch.no_grad()
|
||||
def forward( # type: ignore[override]
|
||||
self,
|
||||
input_ids: Optional[torch.LongTensor] = None,
|
||||
attention_mask: Optional[torch.Tensor] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""Lens-specific forward.
|
||||
|
||||
When ``input_ids`` and ``attention_mask`` are provided AND
|
||||
``set_selected_layers(...)`` has been called, this returns the list of
|
||||
hidden states at the configured selected layers (the Lens feature
|
||||
extraction path).
|
||||
|
||||
Otherwise, falls back to ``GptOssForCausalLM.forward`` so that
|
||||
``generate(...)`` (used by the prompt reasoner) still works unchanged.
|
||||
"""
|
||||
is_lens_feature_call = (
|
||||
input_ids is not None
|
||||
and attention_mask is not None
|
||||
and hasattr(self, "_lens_selected_layers")
|
||||
and not args
|
||||
and not kwargs
|
||||
)
|
||||
|
||||
target_device = self.model.embed_tokens.weight.device
|
||||
if input_ids is not None and input_ids.device != target_device:
|
||||
input_ids = input_ids.to(target_device)
|
||||
if attention_mask is not None and attention_mask.device != target_device:
|
||||
attention_mask = attention_mask.to(target_device)
|
||||
|
||||
if not is_lens_feature_call:
|
||||
return super().forward(input_ids, attention_mask, *args, **kwargs)
|
||||
|
||||
model = self.model
|
||||
inputs_embeds = model.embed_tokens(input_ids)
|
||||
position_ids = torch.arange(
|
||||
inputs_embeds.shape[1], device=inputs_embeds.device
|
||||
).unsqueeze(0).expand_as(input_ids)
|
||||
|
||||
mask_kwargs = {
|
||||
"config": model.config,
|
||||
"inputs_embeds": inputs_embeds,
|
||||
"attention_mask": attention_mask,
|
||||
"past_key_values": None,
|
||||
"position_ids": position_ids,
|
||||
}
|
||||
causal_mask_mapping = {
|
||||
"full_attention": create_causal_mask(**mask_kwargs),
|
||||
"sliding_attention": create_sliding_window_causal_mask(**mask_kwargs),
|
||||
}
|
||||
|
||||
hidden_states = inputs_embeds
|
||||
position_embeddings = model.rotary_emb(hidden_states, position_ids)
|
||||
|
||||
captured: List[torch.Tensor] = [None] * len(self._lens_selected_layers)
|
||||
index_lookup = {idx: pos for pos, idx in enumerate(self._lens_selected_layers)}
|
||||
|
||||
for i, decoder_layer in enumerate(model.layers):
|
||||
hidden_states = decoder_layer(
|
||||
hidden_states,
|
||||
attention_mask=causal_mask_mapping[model.config.layer_types[i]],
|
||||
position_embeddings=position_embeddings,
|
||||
position_ids=position_ids,
|
||||
past_key_values=None,
|
||||
use_cache=False,
|
||||
)
|
||||
if i in index_lookup:
|
||||
captured[index_lookup[i]] = hidden_states
|
||||
if i == self._lens_max_layer:
|
||||
break
|
||||
|
||||
for pos, layer_idx in enumerate(self._lens_selected_layers):
|
||||
if captured[pos] is None:
|
||||
raise RuntimeError(
|
||||
f"Failed to capture hidden state for layer {layer_idx}"
|
||||
)
|
||||
return captured
|
||||
|
||||
def encode_layers(
|
||||
self,
|
||||
input_ids: torch.LongTensor,
|
||||
attention_mask: torch.Tensor,
|
||||
) -> List[torch.Tensor]:
|
||||
"""Backwards-compatible alias for the Lens feature path.
|
||||
|
||||
Kept so existing call sites (``LensPipeline._get_text_embeddings``,
|
||||
external users) keep working. New code should call the encoder
|
||||
directly: ``encoder(input_ids, attention_mask)``.
|
||||
"""
|
||||
if not hasattr(self, "_lens_selected_layers"):
|
||||
raise RuntimeError("Call set_selected_layers(...) before encode_layers().")
|
||||
return self(input_ids=input_ids, attention_mask=attention_mask)
|
||||
@@ -0,0 +1,554 @@
|
||||
"""Lens denoising transformer (DiT).
|
||||
|
||||
The model uses a double-stream architecture with joint image+text attention,
|
||||
RoPE on both streams, and SwiGLU MLPs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
||||
from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin
|
||||
from diffusers.models.attention import FeedForward
|
||||
from diffusers.models.cache_utils import CacheMixin
|
||||
from diffusers.models.embeddings import TimestepEmbedding, Timesteps
|
||||
from diffusers.models.modeling_utils import ModelMixin
|
||||
from diffusers.models.normalization import AdaLayerNormContinuous, RMSNorm
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Embeddings & RoPE
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_timestep_embedding(
|
||||
timesteps: torch.Tensor,
|
||||
embedding_dim: int,
|
||||
flip_sin_to_cos: bool = False,
|
||||
downscale_freq_shift: float = 1.0,
|
||||
scale: float = 1.0,
|
||||
max_period: int = 10000,
|
||||
) -> torch.Tensor:
|
||||
"""Sinusoidal timestep embeddings (DDPM-style)."""
|
||||
assert timesteps.ndim == 1, "Timesteps should be 1-D"
|
||||
half_dim = embedding_dim // 2
|
||||
exponent = -math.log(max_period) * torch.arange(
|
||||
0, half_dim, dtype=torch.float32, device=timesteps.device
|
||||
)
|
||||
exponent = exponent / (half_dim - downscale_freq_shift)
|
||||
emb = torch.exp(exponent).to(timesteps.dtype)
|
||||
emb = timesteps[:, None].float() * emb[None, :]
|
||||
emb = scale * emb
|
||||
emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
|
||||
if flip_sin_to_cos:
|
||||
emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1)
|
||||
if embedding_dim % 2 == 1:
|
||||
emb = F.pad(emb, (0, 1, 0, 0))
|
||||
return emb
|
||||
|
||||
|
||||
def apply_rotary_emb_lens(
|
||||
x: torch.Tensor,
|
||||
freqs_cis: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Apply complex-valued RoPE (Lens variant).
|
||||
|
||||
Args:
|
||||
x: [B, S, H, D] query or key tensor.
|
||||
freqs_cis: [S, D/2] complex tensor of rotation factors.
|
||||
"""
|
||||
x_complex = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
|
||||
freqs_cis = freqs_cis.unsqueeze(1) # broadcast over heads
|
||||
x_out = torch.view_as_real(x_complex * freqs_cis).flatten(3)
|
||||
return x_out.type_as(x)
|
||||
|
||||
|
||||
class GateMLP(nn.Module):
|
||||
"""SwiGLU MLP used by the transformer blocks."""
|
||||
|
||||
def __init__(self, dim: int, hidden_dim: int) -> None:
|
||||
super().__init__()
|
||||
self.w1 = nn.Linear(dim, hidden_dim, bias=False)
|
||||
self.w2 = nn.Linear(hidden_dim, dim, bias=False)
|
||||
self.w3 = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return self.w2(F.silu(self.w1(x)) * self.w3(x))
|
||||
|
||||
|
||||
class LensTimestepProjEmbeddings(nn.Module):
|
||||
def __init__(self, embedding_dim: int) -> None:
|
||||
super().__init__()
|
||||
self.time_proj = Timesteps(
|
||||
num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0, scale=1000
|
||||
)
|
||||
self.timestep_embedder = TimestepEmbedding(
|
||||
in_channels=256, time_embed_dim=embedding_dim
|
||||
)
|
||||
|
||||
def forward(self, timestep: torch.Tensor, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
proj = self.time_proj(timestep)
|
||||
return self.timestep_embedder(proj.to(dtype=hidden_states.dtype))
|
||||
|
||||
|
||||
class LensEmbedRope(nn.Module):
|
||||
"""Frame/H/W axial RoPE shared between image and text streams."""
|
||||
|
||||
def __init__(self, theta: int, axes_dim: List[int], scale_rope: bool = False) -> None:
|
||||
super().__init__()
|
||||
self.theta = theta
|
||||
self.axes_dim = axes_dim
|
||||
self.scale_rope = scale_rope
|
||||
pos_index = torch.arange(4096)
|
||||
neg_index = torch.arange(4096).flip(0) * -1 - 1
|
||||
self.pos_freqs = torch.cat(
|
||||
[self._rope_params(pos_index, d, theta) for d in axes_dim], dim=1
|
||||
)
|
||||
self.neg_freqs = torch.cat(
|
||||
[self._rope_params(neg_index, d, theta) for d in axes_dim], dim=1
|
||||
)
|
||||
# Note: we deliberately do NOT register these as buffers - registering
|
||||
# complex tensors as buffers strips the imaginary component on save/load.
|
||||
self.rope_cache: Dict[str, torch.Tensor] = {}
|
||||
|
||||
@staticmethod
|
||||
def _rope_params(index: torch.Tensor, dim: int, theta: int = 10000) -> torch.Tensor:
|
||||
assert dim % 2 == 0
|
||||
freqs = torch.outer(
|
||||
index, 1.0 / torch.pow(theta, torch.arange(0, dim, 2).float().div(dim))
|
||||
)
|
||||
return torch.polar(torch.ones_like(freqs), freqs)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
video_fhw: Union[List[Tuple[int, int, int]], Tuple[int, int, int]],
|
||||
txt_seq_lens: Union[List[int], int],
|
||||
device: torch.device = torch.device("cuda"),
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
if self.pos_freqs.device != device:
|
||||
self.pos_freqs = self.pos_freqs.to(device)
|
||||
self.neg_freqs = self.neg_freqs.to(device)
|
||||
|
||||
if isinstance(video_fhw, list):
|
||||
video_fhw = video_fhw[0]
|
||||
if not isinstance(video_fhw, list):
|
||||
video_fhw = [video_fhw]
|
||||
if not isinstance(txt_seq_lens, list):
|
||||
txt_seq_lens = [txt_seq_lens]
|
||||
assert len(video_fhw) == 1, "video_fhw must have length 1"
|
||||
|
||||
vid_freqs = []
|
||||
max_vid_index = 0
|
||||
for idx, fhw in enumerate(video_fhw):
|
||||
frame, height, width = fhw
|
||||
rope_key = f"{idx}_{height}_{width}"
|
||||
if rope_key not in self.rope_cache:
|
||||
self.rope_cache[rope_key] = (
|
||||
self._compute_video_freqs(frame, height, width, idx=0).to("cpu")
|
||||
)
|
||||
video_freq = self.rope_cache[rope_key].to(device)
|
||||
if self.scale_rope:
|
||||
max_vid_index = max(height // 2, width // 2, max_vid_index)
|
||||
else:
|
||||
max_vid_index = max(height, width, max_vid_index)
|
||||
vid_freqs.append(video_freq)
|
||||
|
||||
max_len = max(txt_seq_lens)
|
||||
txt_freqs = self.pos_freqs[max_vid_index : max_vid_index + max_len, ...]
|
||||
return torch.cat(vid_freqs, dim=0), txt_freqs
|
||||
|
||||
def _compute_video_freqs(self, frame: int, height: int, width: int, idx: int = 0) -> torch.Tensor:
|
||||
seq_lens = frame * height * width
|
||||
freqs_pos = self.pos_freqs.split([d // 2 for d in self.axes_dim], dim=1)
|
||||
freqs_neg = self.neg_freqs.split([d // 2 for d in self.axes_dim], dim=1)
|
||||
|
||||
freqs_frame = freqs_pos[0][idx : idx + frame].view(frame, 1, 1, -1).expand(frame, height, width, -1)
|
||||
if self.scale_rope:
|
||||
freqs_height = torch.cat(
|
||||
[freqs_neg[1][-(height - height // 2) :], freqs_pos[1][: height // 2]], dim=0
|
||||
).view(1, height, 1, -1).expand(frame, height, width, -1)
|
||||
freqs_width = torch.cat(
|
||||
[freqs_neg[2][-(width - width // 2) :], freqs_pos[2][: width // 2]], dim=0
|
||||
).view(1, 1, width, -1).expand(frame, height, width, -1)
|
||||
else:
|
||||
freqs_height = freqs_pos[1][:height].view(1, height, 1, -1).expand(frame, height, width, -1)
|
||||
freqs_width = freqs_pos[2][:width].view(1, 1, width, -1).expand(frame, height, width, -1)
|
||||
|
||||
freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(seq_lens, -1)
|
||||
return freqs.clone().contiguous()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Attention (joint image + text, plain SDPA)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class LensJointAttention(nn.Module):
|
||||
"""Joint image+text attention with fused QKV and SDPA backend."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query_dim: int,
|
||||
added_kv_proj_dim: int,
|
||||
dim_head: int = 64,
|
||||
heads: int = 8,
|
||||
out_dim: Optional[int] = None,
|
||||
eps: float = 1e-5,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.inner_dim = out_dim if out_dim is not None else dim_head * heads
|
||||
self.heads = self.inner_dim // dim_head
|
||||
self.dim_head = dim_head
|
||||
self.out_dim = out_dim if out_dim is not None else query_dim
|
||||
|
||||
self.norm_q = RMSNorm(dim_head, eps=eps)
|
||||
self.norm_k = RMSNorm(dim_head, eps=eps)
|
||||
self.norm_added_q = RMSNorm(dim_head, eps=eps)
|
||||
self.norm_added_k = RMSNorm(dim_head, eps=eps)
|
||||
|
||||
self.img_qkv = nn.Linear(query_dim, 3 * self.inner_dim, bias=True)
|
||||
self.txt_qkv = nn.Linear(added_kv_proj_dim, 3 * self.inner_dim, bias=True)
|
||||
|
||||
self.to_out = nn.ModuleList([nn.Linear(self.inner_dim, self.out_dim, bias=True), nn.Identity()])
|
||||
self.to_add_out = nn.Linear(self.inner_dim, query_dim, bias=True)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
encoder_hidden_states: torch.Tensor,
|
||||
image_rotary_emb: Tuple[torch.Tensor, torch.Tensor],
|
||||
attention_mask: Optional[torch.Tensor] = None,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
bsz, seq_img, _ = hidden_states.shape
|
||||
seq_txt = encoder_hidden_states.shape[1]
|
||||
|
||||
# Fused QKV per stream -> split.
|
||||
img_qkv = self.img_qkv(hidden_states).view(bsz, seq_img, 3, self.heads, self.dim_head)
|
||||
txt_qkv = self.txt_qkv(encoder_hidden_states).view(bsz, seq_txt, 3, self.heads, self.dim_head)
|
||||
img_q, img_k, img_v = img_qkv.unbind(dim=2)
|
||||
txt_q, txt_k, txt_v = txt_qkv.unbind(dim=2)
|
||||
|
||||
# QK RMSNorm.
|
||||
img_q = self.norm_q(img_q)
|
||||
img_k = self.norm_k(img_k)
|
||||
txt_q = self.norm_added_q(txt_q)
|
||||
txt_k = self.norm_added_k(txt_k)
|
||||
|
||||
# RoPE.
|
||||
img_freqs, txt_freqs = image_rotary_emb
|
||||
if img_freqs.shape[0] < seq_img:
|
||||
raise ValueError(
|
||||
f"Image RoPE length {img_freqs.shape[0]} is shorter than "
|
||||
f"image sequence length {seq_img}."
|
||||
)
|
||||
img_freqs = img_freqs[:seq_img]
|
||||
img_q = apply_rotary_emb_lens(img_q, img_freqs)
|
||||
img_k = apply_rotary_emb_lens(img_k, img_freqs)
|
||||
if seq_txt > 0:
|
||||
if txt_freqs.shape[0] < seq_txt:
|
||||
raise ValueError(
|
||||
f"Text RoPE length {txt_freqs.shape[0]} is shorter than "
|
||||
f"text sequence length {seq_txt}."
|
||||
)
|
||||
txt_freqs = txt_freqs[:seq_txt]
|
||||
txt_q = apply_rotary_emb_lens(txt_q, txt_freqs)
|
||||
txt_k = apply_rotary_emb_lens(txt_k, txt_freqs)
|
||||
|
||||
# Joint sequence per sample, then SDPA in [B, H, S, D] layout.
|
||||
q = torch.cat([img_q, txt_q], dim=1).transpose(1, 2)
|
||||
k = torch.cat([img_k, txt_k], dim=1).transpose(1, 2)
|
||||
v = torch.cat([img_v, txt_v], dim=1).transpose(1, 2)
|
||||
|
||||
if attention_mask is not None:
|
||||
expected_mask_shape = (bsz, 1, 1, seq_img + seq_txt)
|
||||
if attention_mask.shape != expected_mask_shape:
|
||||
raise ValueError(
|
||||
f"attention_mask must have shape {expected_mask_shape}, "
|
||||
f"got {tuple(attention_mask.shape)}."
|
||||
)
|
||||
attention_mask = attention_mask.to(q.dtype)
|
||||
out = F.scaled_dot_product_attention(q, k, v, attn_mask=attention_mask)
|
||||
out = out.transpose(1, 2).reshape(bsz, seq_img + seq_txt, -1)
|
||||
|
||||
img_out = self.to_out[1](self.to_out[0](out[:, :seq_img, :]))
|
||||
txt_out = self.to_add_out(out[:, seq_img:, :])
|
||||
return img_out, txt_out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transformer block
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class LensTransformerBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
num_attention_heads: int,
|
||||
attention_head_dim: int,
|
||||
eps: float = 1e-6,
|
||||
rms_norm: bool = False,
|
||||
gate_mlp: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.attn = LensJointAttention(
|
||||
query_dim=dim,
|
||||
added_kv_proj_dim=dim,
|
||||
dim_head=attention_head_dim,
|
||||
heads=num_attention_heads,
|
||||
out_dim=dim,
|
||||
eps=eps,
|
||||
)
|
||||
|
||||
norm_cls = (lambda d: RMSNorm(d, eps=eps)) if rms_norm else (
|
||||
lambda d: nn.LayerNorm(d, elementwise_affine=False, eps=eps)
|
||||
)
|
||||
if gate_mlp:
|
||||
mlp_cls = lambda: GateMLP(dim, int(dim / 3 * 8)) # pylint: disable=unnecessary-lambda-assignment
|
||||
else:
|
||||
mlp_cls = lambda: FeedForward(dim=dim, dim_out=dim, activation_fn="gelu-approximate") # pylint: disable=unnecessary-lambda-assignment
|
||||
|
||||
self.img_mod = nn.Sequential(nn.SiLU(), nn.Linear(dim, 6 * dim, bias=True))
|
||||
self.img_norm1 = norm_cls(dim)
|
||||
self.img_norm2 = norm_cls(dim)
|
||||
self.img_mlp = mlp_cls()
|
||||
|
||||
self.txt_mod = nn.Sequential(nn.SiLU(), nn.Linear(dim, 6 * dim, bias=True))
|
||||
self.txt_norm1 = norm_cls(dim)
|
||||
self.txt_norm2 = norm_cls(dim)
|
||||
self.txt_mlp = mlp_cls()
|
||||
|
||||
@staticmethod
|
||||
def _modulate(x: torch.Tensor, mod_params: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
shift, scale, gate = mod_params.chunk(3, dim=-1)
|
||||
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1), gate.unsqueeze(1)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
encoder_hidden_states: torch.Tensor,
|
||||
temb: torch.Tensor,
|
||||
image_rotary_emb: Tuple[torch.Tensor, torch.Tensor],
|
||||
attention_mask: Optional[torch.Tensor] = None,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
img_mod1, img_mod2 = self.img_mod(temb).chunk(2, dim=-1)
|
||||
txt_mod1, txt_mod2 = self.txt_mod(temb).chunk(2, dim=-1)
|
||||
|
||||
img_modulated, img_gate1 = self._modulate(self.img_norm1(hidden_states), img_mod1)
|
||||
txt_modulated, txt_gate1 = self._modulate(self.txt_norm1(encoder_hidden_states), txt_mod1)
|
||||
|
||||
img_attn, txt_attn = self.attn(
|
||||
hidden_states=img_modulated,
|
||||
encoder_hidden_states=txt_modulated,
|
||||
image_rotary_emb=image_rotary_emb,
|
||||
attention_mask=attention_mask,
|
||||
)
|
||||
|
||||
hidden_states = hidden_states + img_gate1 * img_attn
|
||||
encoder_hidden_states = encoder_hidden_states + txt_gate1 * txt_attn
|
||||
|
||||
img_modulated2, img_gate2 = self._modulate(self.img_norm2(hidden_states), img_mod2)
|
||||
hidden_states = hidden_states + img_gate2 * self.img_mlp(img_modulated2)
|
||||
|
||||
txt_modulated2, txt_gate2 = self._modulate(self.txt_norm2(encoder_hidden_states), txt_mod2)
|
||||
encoder_hidden_states = encoder_hidden_states + txt_gate2 * self.txt_mlp(txt_modulated2)
|
||||
|
||||
return encoder_hidden_states, hidden_states
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Top-level model
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class LensTransformer2DModel(
|
||||
ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin, CacheMixin
|
||||
):
|
||||
"""The Lens text-to-image DiT.
|
||||
|
||||
Supports a single conditioning stream of multi-layer text features. The
|
||||
text features are normalized per layer, concatenated along the channel
|
||||
axis, and projected to `inner_dim` before joining the image stream.
|
||||
"""
|
||||
|
||||
_supports_gradient_checkpointing = True
|
||||
_no_split_modules = ["LensTransformerBlock"]
|
||||
_skip_layerwise_casting_patterns = ["pos_embed", "norm"]
|
||||
_repeated_blocks = ["LensTransformerBlock"]
|
||||
|
||||
@register_to_config
|
||||
def __init__(
|
||||
self,
|
||||
patch_size: int = 2,
|
||||
in_channels: int = 128,
|
||||
out_channels: Optional[int] = 32,
|
||||
num_layers: int = 48,
|
||||
attention_head_dim: int = 64,
|
||||
num_attention_heads: int = 24,
|
||||
inner_dim: int = 1536, # pylint: disable=unused-argument
|
||||
enc_hidden_dim: int = 2880,
|
||||
axes_dims_rope: Tuple[int, int, int] = (8, 28, 28),
|
||||
gate_mlp: bool = True,
|
||||
rms_norm: bool = True,
|
||||
multi_layer_encoder_feature: bool = True,
|
||||
selected_layer_index: Tuple[int, ...] = (5, 11, 17, 23),
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.in_channels = in_channels
|
||||
self.out_channels = out_channels or in_channels
|
||||
self.inner_dim = num_attention_heads * attention_head_dim
|
||||
self.multi_layer_encoder_feature = multi_layer_encoder_feature
|
||||
self.selected_layer_index = list(selected_layer_index)
|
||||
|
||||
self.pos_embed = LensEmbedRope(theta=10000, axes_dim=list(axes_dims_rope), scale_rope=True)
|
||||
self.time_text_embed = LensTimestepProjEmbeddings(embedding_dim=self.inner_dim)
|
||||
|
||||
if self.multi_layer_encoder_feature:
|
||||
self.txt_norm = nn.ModuleList(
|
||||
[RMSNorm(enc_hidden_dim, eps=1e-5) for _ in self.selected_layer_index]
|
||||
)
|
||||
self.txt_in = nn.Linear(enc_hidden_dim * len(self.selected_layer_index), self.inner_dim)
|
||||
else:
|
||||
self.txt_norm = RMSNorm(enc_hidden_dim, eps=1e-5)
|
||||
self.txt_in = nn.Linear(enc_hidden_dim, self.inner_dim)
|
||||
|
||||
self.img_in = nn.Linear(in_channels, self.inner_dim)
|
||||
|
||||
self.transformer_blocks = nn.ModuleList(
|
||||
[
|
||||
LensTransformerBlock(
|
||||
dim=self.inner_dim,
|
||||
num_attention_heads=num_attention_heads,
|
||||
attention_head_dim=attention_head_dim,
|
||||
rms_norm=rms_norm,
|
||||
gate_mlp=gate_mlp,
|
||||
)
|
||||
for _ in range(num_layers)
|
||||
]
|
||||
)
|
||||
self.norm_out = AdaLayerNormContinuous(
|
||||
self.inner_dim, self.inner_dim, elementwise_affine=False, eps=1e-6
|
||||
)
|
||||
self.proj_out = nn.Linear(self.inner_dim, patch_size * patch_size * self.out_channels, bias=True)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
encoder_hidden_states: Union[torch.Tensor, List[torch.Tensor]],
|
||||
encoder_hidden_states_mask: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
img_shapes: List[Tuple[int, int, int]],
|
||||
attention_kwargs: Optional[Dict[str, Any]] = None, # pylint: disable=unused-argument
|
||||
) -> torch.Tensor:
|
||||
"""Forward pass.
|
||||
|
||||
Args:
|
||||
hidden_states: [B, S_img, in_channels] image latents.
|
||||
encoder_hidden_states: either a Tensor [B, S_txt, enc_dim]
|
||||
(single-layer) or a list of such
|
||||
tensors (multi-layer).
|
||||
encoder_hidden_states_mask: bool [B, S_txt] (True = valid).
|
||||
timestep: [B] in [0, 1].
|
||||
img_shapes: list with a single (frame, h_lat, w_lat).
|
||||
"""
|
||||
bsz, img_len, _ = hidden_states.shape
|
||||
if self.multi_layer_encoder_feature:
|
||||
if not isinstance(encoder_hidden_states, (list, tuple)):
|
||||
raise ValueError(
|
||||
"multi_layer_encoder_feature=True expects a list of "
|
||||
"per-layer text tensors."
|
||||
)
|
||||
if len(encoder_hidden_states) != len(self.selected_layer_index):
|
||||
raise ValueError(
|
||||
f"Expected {len(self.selected_layer_index)} text feature "
|
||||
f"layers, got {len(encoder_hidden_states)}."
|
||||
)
|
||||
text_seq_len = encoder_hidden_states[0].shape[1]
|
||||
for i, feat in enumerate(encoder_hidden_states):
|
||||
if feat.shape[0] != bsz:
|
||||
raise ValueError(
|
||||
f"Text feature layer {i} batch size {feat.shape[0]} "
|
||||
f"does not match hidden_states batch size {bsz}."
|
||||
)
|
||||
if feat.shape[1] != text_seq_len:
|
||||
raise ValueError(
|
||||
f"Text feature layer {i} sequence length {feat.shape[1]} "
|
||||
f"does not match layer 0 length {text_seq_len}."
|
||||
)
|
||||
else:
|
||||
if not isinstance(encoder_hidden_states, torch.Tensor):
|
||||
raise ValueError(
|
||||
"multi_layer_encoder_feature=False expects a single text "
|
||||
"feature tensor."
|
||||
)
|
||||
if encoder_hidden_states.shape[0] != bsz:
|
||||
raise ValueError(
|
||||
f"Text feature batch size {encoder_hidden_states.shape[0]} "
|
||||
f"does not match hidden_states batch size {bsz}."
|
||||
)
|
||||
text_seq_len = encoder_hidden_states.shape[1]
|
||||
if encoder_hidden_states_mask.shape != (bsz, text_seq_len):
|
||||
raise ValueError(
|
||||
"encoder_hidden_states_mask must have shape "
|
||||
f"{(bsz, text_seq_len)}, got {tuple(encoder_hidden_states_mask.shape)}."
|
||||
)
|
||||
attention_mask = self._build_joint_attention_mask(
|
||||
encoder_hidden_states_mask, img_len
|
||||
)
|
||||
|
||||
hidden_states = self.img_in(hidden_states)
|
||||
timestep = timestep.to(hidden_states.dtype)
|
||||
|
||||
if self.multi_layer_encoder_feature:
|
||||
normed = [
|
||||
self.txt_norm[i](encoder_hidden_states[i])
|
||||
for i in range(len(self.selected_layer_index))
|
||||
]
|
||||
encoder_hidden_states = torch.cat(normed, dim=-1)
|
||||
else:
|
||||
encoder_hidden_states = self.txt_norm(encoder_hidden_states)
|
||||
encoder_hidden_states = self.txt_in(encoder_hidden_states)
|
||||
|
||||
temb = self.time_text_embed(timestep, hidden_states)
|
||||
|
||||
image_rotary_emb = self.pos_embed(
|
||||
img_shapes, [text_seq_len], device=hidden_states.device
|
||||
)
|
||||
|
||||
for block in self.transformer_blocks:
|
||||
encoder_hidden_states, hidden_states = block(
|
||||
hidden_states=hidden_states,
|
||||
encoder_hidden_states=encoder_hidden_states,
|
||||
temb=temb,
|
||||
image_rotary_emb=image_rotary_emb,
|
||||
attention_mask=attention_mask,
|
||||
)
|
||||
|
||||
hidden_states = self.norm_out(hidden_states, temb)
|
||||
return self.proj_out(hidden_states)
|
||||
|
||||
@staticmethod
|
||||
def _build_joint_attention_mask(
|
||||
text_mask: torch.Tensor, img_len: int
|
||||
) -> torch.Tensor:
|
||||
"""Additive joint mask of shape ``[B, 1, 1, img_len + S_txt]``.
|
||||
|
||||
Image tokens are always valid; text positions follow ``text_mask``.
|
||||
Padded positions hold ``-inf`` so SDPA's softmax masks them out.
|
||||
"""
|
||||
if text_mask.dtype != torch.bool:
|
||||
text_mask = text_mask.bool()
|
||||
bsz = text_mask.shape[0]
|
||||
img_ones = torch.ones(
|
||||
(bsz, img_len), dtype=torch.bool, device=text_mask.device
|
||||
)
|
||||
joint = torch.cat([img_ones, text_mask], dim=1)
|
||||
additive = torch.zeros_like(joint, dtype=torch.float32)
|
||||
additive.masked_fill_(~joint, float("-inf"))
|
||||
return additive[:, None, None, :]
|
||||
@@ -0,0 +1,38 @@
|
||||
from modules import shared, devices, sd_models, model_quant, sd_hijack_te, sd_hijack_vae
|
||||
from modules.logger import log
|
||||
from pipelines import generic
|
||||
|
||||
|
||||
def load_lens(checkpoint_info, diffusers_load_config=None):
|
||||
if diffusers_load_config is None:
|
||||
diffusers_load_config = {}
|
||||
|
||||
repo_id = sd_models.path_to_repo(checkpoint_info)
|
||||
sd_models.hf_auth_check(checkpoint_info)
|
||||
|
||||
load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False)
|
||||
log.debug(f'Load model: type=Lens repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}')
|
||||
|
||||
from pipelines import lens
|
||||
|
||||
transformer = generic.load_transformer(repo_id, cls_name=lens.LensTransformer2DModel, load_config=diffusers_load_config)
|
||||
text_encoder = generic.load_text_encoder(repo_id, cls_name=lens.LensGptOssEncoder, load_config=diffusers_load_config, allow_quant=False) # te is prequantized using mxfp4
|
||||
|
||||
if not shared.opts.model_lens_enable_pe:
|
||||
load_args['reasoner'] = None
|
||||
|
||||
pipe = lens.LensPipeline.from_pretrained(
|
||||
repo_id,
|
||||
transformer=transformer,
|
||||
text_encoder=text_encoder,
|
||||
cache_dir=shared.opts.diffusers_dir,
|
||||
**load_args,
|
||||
)
|
||||
pipe.task_args = {
|
||||
"output_type": "np",
|
||||
}
|
||||
|
||||
sd_hijack_te.init_hijack(pipe)
|
||||
sd_hijack_vae.init_hijack(pipe)
|
||||
devices.torch_gc(force=True, reason="load")
|
||||
return pipe
|
||||
Reference in New Issue
Block a user