File size: 13,029 Bytes
1624300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
"""Stable Diffusion 1.5 engine with the same interface and rules as the FLUX engine.

The loop runs Euler in sigma space, so the update reads like the FLUX one:
x = x + (sigma_next - sigma) * eps. The network is told the time as a timestep in [0, 1000).
"""

from __future__ import annotations

import threading
from collections import OrderedDict
from contextlib import contextmanager

import torch
from PIL import Image

import engine as E
from engine import Clock, Settings

DECODE_PIXELS = E.DECODE_PIXELS


class SD15Engine(E.Engine):
    latent_factor = 8               # VAE scale, no patching
    standard_layers = (12,)         # CLIP's last layer, as in the pipeline

    def __init__(self, pipe, device: str | torch.device = "cuda"):
        from diffusers import EulerDiscreteScheduler

        self.pipe = pipe
        self.device = torch.device(device)
        self.clock = Clock()
        self._emb_cache: OrderedDict = OrderedDict()
        self._lock = threading.Lock()
        self.post_step: list = []       # functions latent -> latent after every sampler step (Tab 4)
        self.encode_tag = ""            # part of the encoding cache key while a tool changes CLIP
        self.run_notes: list = []
        self.dtype = pipe.unet.dtype
        # Euler in sigma space, configured from the checkpoint's scheduler (same betas, same offset).
        self._scheduler_config = EulerDiscreteScheduler.from_config(pipe.scheduler.config).config
        self._Scheduler = EulerDiscreteScheduler
        for m in (pipe.unet, pipe.text_encoder, pipe.vae):
            m.requires_grad_(False)
            m.eval()

    @classmethod
    def from_pretrained(cls, model_id: str = E.MODEL_ID, device: str = "cuda") -> "SD15Engine":
        from diffusers import StableDiffusionPipeline

        # bfloat16 rather than float16: bent activations overflow less.
        dtype = torch.bfloat16 if (device != "cuda" or torch.cuda.is_bf16_supported()) else torch.float16
        pipe = StableDiffusionPipeline.from_pretrained(model_id, dtype=dtype, variant="fp16", safety_checker=None,
                                                       feature_extractor=None, requires_safety_checker=False)
        pipe = pipe.to(device)
        pipe.vae.to(torch.float32)
        # Typed sizes go up to 1536 px, where the fp32 decoder would run out of memory. SD's VAE would
        # tile from 512 px on, so tiles start only above 1024 px and smaller images decode as before.
        pipe.vae.enable_tiling()
        pipe.vae.tile_sample_min_size = 1024
        pipe.vae.tile_latent_min_size = 1024 // 8
        return cls(pipe, device)

    # ---------------------------------------------------------------- text

    def encode(self, prompt: str, layers: tuple = (12,), max_len: int = 77,
               shuffle_seed: int | None = None) -> torch.Tensor:
        """CLIP hidden states (1, 77, 768) after the final layer norm, read at one layer. Cached.

        shuffle_seed permutes the prompt's tokens before the encoder reads them (R4).
        """
        if len(layers) != 1:
            raise ValueError("SD 1.5 reads the text encoder at exactly one layer")
        key = (prompt, tuple(layers), max_len, shuffle_seed, self.encode_tag)
        with self._lock:
            if key in self._emb_cache:
                self._emb_cache.move_to_end(key)
                return self._emb_cache[key]
        te, tok = self.pipe.text_encoder, self.pipe.tokenizer
        ids = tok(prompt, padding="max_length", max_length=tok.model_max_length, truncation=True,
                  return_tensors="pt").input_ids
        pos = self.content_positions(prompt)
        if shuffle_seed is not None and len(pos) > 1:
            perm = torch.randperm(len(pos), generator=torch.Generator("cpu").manual_seed(shuffle_seed))
            idx = torch.tensor(pos)
            ids[0, idx] = ids[0, idx[perm]]
        ids = ids.to(self.device)
        with torch.inference_mode():
            out = te(ids, output_hidden_states=True)
            # Layer 12 gives exactly the pipeline's prompt_embeds (last_hidden_state).
            # transformers 5 keeps the norm on the model itself, older versions on .text_model.
            norm = te.final_layer_norm if hasattr(te, "final_layer_norm") else te.text_model.final_layer_norm
            emb = norm(out.hidden_states[layers[0]]).to(self.dtype)
        with self._lock:
            self._emb_cache[key] = emb
            while len(self._emb_cache) > E.EMB_CACHE_SIZE:
                self._emb_cache.popitem(last=False)
        return emb

    def prompt_embeddings(self, s: Settings, max_len: int) -> tuple[torch.Tensor, torch.Tensor]:
        # The UNet's cross-attention has no positions on the text side, so permuting the encoded
        # vectors would change nothing. R4 shuffles the word order before CLIP reads it instead.
        seed = s.seed + E.SEED_SHUFFLE if s.emb_shuffle else None
        return (self.encode(s.prompt_a, s.te_layers, max_len, seed).float(),
                self.encode(s.prompt_b, s.te_layers, max_len, seed).float())

    def shuffle_cond(self, s: Settings, cond: torch.Tensor, max_len: int) -> torch.Tensor:
        return cond                                        # already shuffled before the encoder

    def content_positions(self, prompt: str, max_len: int = 77) -> list[int]:
        """Positions of the prompt's tokens: after the start token, before the end token and the padding."""
        n = len(self.pipe.tokenizer(prompt, add_special_tokens=False).input_ids)
        return list(range(1, 1 + min(n, self.pipe.tokenizer.model_max_length - 2)))

    # ---------------------------------------------------------------- blocks

    def block(self, block_id: str):
        unet = self.pipe.unet
        if block_id == "mid":
            return unet.mid_block
        kind, k = block_id.split(".")
        return {"down": unet.down_blocks, "up": unet.up_blocks}[kind][int(k)]

    def block_ids(self) -> list[str]:
        unet = self.pipe.unet
        return ([f"down.{k}" for k in range(len(unet.down_blocks))] + ["mid"]
                + [f"up.{k}" for k in range(len(unet.up_blocks))])

    def reads_text(self, block_id: str) -> bool:
        return bool(getattr(self.block(block_id), "has_cross_attention", False))

    def text_bend_note(self, s: Settings) -> str | None:
        if s.bend_stream == "text" and not self.reads_text(s.bend_block):
            return ("Dieser Block liest den Text nicht, er hat keine Cross-Attention. "
                    "Der Eingriff ändert deshalb nichts. Wählt einen anderen Block.")
        return None

    @contextmanager
    def interventions(self, s: Settings, lat_h: int, lat_w: int, txt_len: int):
        """R8 and R9 as in the FLUX engine, plus the tools of Tab 4."""
        import tools_sd15

        with super().interventions(s, lat_h, lat_w, txt_len), tools_sd15.apply(self, s) as notes:
            self.run_notes += notes
            yield

    @contextmanager
    def bending(self, s: Settings, lat_h: int, lat_w: int, txt_len: int):
        """R8. Image stream: the block's output feature map. Text stream: the text the block attends to."""
        if not s.bend_block or s.bend_op == "none" or s.bend_strength == 0:
            yield
            return
        op, st, image = s.bend_op, float(s.bend_strength), s.bend_stream == "image"
        if op not in E.BEND_OPS:
            raise ValueError(f"unknown bending op {op!r}")
        if not image and op in E.SPATIAL_OPS:
            raise ValueError(f"{op} works on the image stream only")
        if not image and not self.reads_text(s.bend_block):
            yield                                          # nothing to bend, render() adds a note
            return
        block = self.block(s.bend_block)
        masks: dict[int, torch.Tensor] = {}
        gen = torch.Generator(self.device).manual_seed(s.seed + E.SEED_BEND_NOISE) if op == "noise" else None
        clock = self.clock

        def mask(dim: int, device, dtype) -> torch.Tensor | None:
            if op != "ablate":
                return None
            if dim not in masks:                           # fixed per call, one per channel count
                off = torch.randperm(dim, generator=torch.Generator("cpu").manual_seed(s.seed + E.SEED_ABLATE))
                m = torch.ones(dim)
                m[off[:round(min(max(st, 0.0), 1.0) * dim)]] = 0.0   # typed values may leave 0..1
                masks[dim] = m
            return masks[dim].to(device, dtype)

        def bend(h: torch.Tensor) -> torch.Tensor:
            if h.dim() == 4:                               # feature map (B, C, H, W) as tokens (B, H*W, C)
                b, c, hh, ww = h.shape
                tokens = h.permute(0, 2, 3, 1).reshape(b, hh * ww, c)
                out = E.bend_tensor(tokens, op, st, (hh, ww), mask(c, h.device, h.dtype), gen)
                return out.reshape(b, hh, ww, c).permute(0, 3, 1, 2).contiguous()
            return E.bend_tensor(h, op, st, None, mask(h.shape[-1], h.device, h.dtype), gen)

        def active() -> bool:
            return s.bend_from <= clock.progress <= s.bend_to

        if image:
            def hook(module, args, output):
                if not active():
                    return None
                if isinstance(output, tuple):              # down blocks: (hidden states, skip connections)
                    return (bend(output[0]),) + tuple(output[1:])
                return bend(output)

            handle = block.register_forward_hook(hook)
        else:
            def pre_hook(module, args, kwargs):
                if not active() or kwargs.get("encoder_hidden_states") is None:
                    return None
                return args, {**kwargs, "encoder_hidden_states": bend(kwargs["encoder_hidden_states"])}

            handle = block.register_forward_pre_hook(pre_hook, with_kwargs=True)
        try:
            yield
        finally:
            handle.remove()

    # ---------------------------------------------------------------- loop

    def denoise(self, s: Settings, cond: torch.Tensor, uncond: torch.Tensor, seeds: list[int],
                guidances: list[float], on_step=None) -> torch.Tensor:
        """Euler in sigma space for one chunk. Returns latents (n, 4, h, w) in float32."""
        dev, N = self.device, s.steps
        n = len(seeds)
        lat_h, lat_w = s.height // 8, s.width // 8
        noise = torch.stack([torch.randn((4, lat_h, lat_w), generator=torch.Generator("cpu").manual_seed(sd))
                             for sd in seeds]).to(dev)        # float32, device independent
        sched = self._Scheduler.from_config(self._scheduler_config)   # fresh: the scheduler is stateful
        sched.set_timesteps(N, device=dev)
        sig = sched.sigmas.float().cpu().tolist()             # N + 1 values ending in 0
        lat = noise * sched.init_noise_sigma
        n_run = max(1, round(N * s.stop_at))

        enc = torch.cat([cond, uncond.expand(n, -1, -1)]).to(self.dtype)
        g = torch.tensor(guidances, device=dev, dtype=torch.float32).view(-1, 1, 1, 1)
        x0_pred = None
        for i, t in enumerate(sched.timesteps[:n_run]):
            self.clock.progress = i / N
            sigma = sig[i]
            t_model = float(t) * s.sigma_mult + s.sigma_offset * 1000   # R6: the model is told a different time
            x = sched.scale_model_input(lat, t)                         # x / sqrt(sigma^2 + 1)
            x = torch.cat([x, x]).to(self.dtype)
            eps = self.pipe.unet(x, torch.full((2 * n,), t_model, device=dev), encoder_hidden_states=enc,
                                 return_dict=False)[0].float()
            e_c, e_u = eps.chunk(2)
            eps = e_u + g * (e_c - e_u)                                 # R3: per-sample guidance
            x0_pred = lat - sigma * eps
            lat = sched.step(eps, t, lat, return_dict=False)[0]         # x + (sigma_next - sigma) * eps
            for fn in self.post_step:                                   # Tab 4, e.g. the mirror
                lat = fn(lat)
            if on_step is not None:
                on_step()
        return x0_pred if (s.show_x0 and n_run < N) else lat

    def decode(self, final: torch.Tensor, lat_h: int, lat_w: int) -> tuple[list[Image.Image], list[bool]]:
        vae = self.pipe.vae
        n = final.shape[0]
        finite = torch.isfinite(final).flatten(1).all(1).tolist()
        lat = torch.nan_to_num(final.float(), 0.0, 0.0, 0.0) / vae.config.scaling_factor
        per_call = max(1, DECODE_PIXELS // (lat_h * lat_w * 64))
        images = []
        for k in range(0, n, per_call):
            img = vae.decode(lat[k:k + per_call].to(vae.dtype), return_dict=False)[0]
            img = torch.nan_to_num(img.float(), 0.0, 0.0, 0.0)
            images += self.pipe.image_processor.postprocess(img, output_type="pil",
                                                           do_denormalize=[True] * img.shape[0])
        return images, finite