dipta007 commited on
Commit
fcc874d
·
verified ·
1 Parent(s): 9ae706e

Add the OracleZoom zoom demo

Browse files
.gitattributes CHANGED
@@ -33,3 +33,15 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ samples/0064.png filter=lfs diff=lfs merge=lfs -text
37
+ samples/0245.png filter=lfs diff=lfs merge=lfs -text
38
+ samples/0393.png filter=lfs diff=lfs merge=lfs -text
39
+ samples/0457.png filter=lfs diff=lfs merge=lfs -text
40
+ samples/0479.png filter=lfs diff=lfs merge=lfs -text
41
+ samples/example_16x.png filter=lfs diff=lfs merge=lfs -text
42
+ samples/example_1x.png filter=lfs diff=lfs merge=lfs -text
43
+ samples/example_256x.png filter=lfs diff=lfs merge=lfs -text
44
+ samples/example_256x_input.png filter=lfs diff=lfs merge=lfs -text
45
+ samples/example_4x.png filter=lfs diff=lfs merge=lfs -text
46
+ samples/example_64x.png filter=lfs diff=lfs merge=lfs -text
47
+ samples/example_zoom.mp4 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,59 @@
1
  ---
2
  title: OracleZoom
3
- emoji: 🐨
4
- colorFrom: gray
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.26.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: OracleZoom
3
+ emoji: 🔎
4
+ colorFrom: yellow
5
+ colorTo: gray
6
  sdk: gradio
7
+ sdk_version: 5.50.0
8
+ python_version: "3.12.12"
9
  app_file: app.py
10
+ startup_duration_timeout: 45m
11
+ pinned: true
12
+ license: mit
13
+ short_description: Zoom any photo to 256x, one 4x step at a time
14
+ tags:
15
+ - super-resolution
16
+ - image-to-image
17
+ - diffusion
18
+ - zoom
19
+ models:
20
+ - dipta007/OracleZoom
21
+ - stabilityai/stable-diffusion-3-medium-diffusers
22
+ - Qwen/Qwen2.5-VL-3B-Instruct
23
+ datasets:
24
+ - dipta007/OracleZoom-4KLSDB-train
25
  ---
26
 
27
+ # OracleZoom
28
+
29
+ Upload a photo, pick a point, and watch it zoom to 256x. Four steps of 4x, each one drawn
30
+ from the last.
31
+
32
+ - Paper: https://arxiv.org/abs/2609.06490
33
+ - Code: https://github.com/dipta007/OracleZoom
34
+ - Project page: https://dipta007.github.io/OracleZoom/
35
+ - Model: https://huggingface.co/dipta007/OracleZoom
36
+
37
+ ## Running it yourself
38
+
39
+ The Space needs an `HF_TOKEN` secret. Stable Diffusion 3-medium is gated, so the token has
40
+ to come from an account that has accepted its licence. Nothing in the code reads the token
41
+ directly; `huggingface_hub` picks up `HF_TOKEN` on its own.
42
+
43
+ Hardware: ZeroGPU. The pipeline holds about 27 GB of weights, well inside the 48 GB that a
44
+ `large` slice gives. ZeroGPU is Gradio-only, so a Docker Space cannot host this.
45
+
46
+ ## What is in here
47
+
48
+ `app.py` is the interface. `zoom.py` is the recursion, flattened from the research repo so it
49
+ runs in memory with no disk round-trips and lets the zoom window sit off centre. `video.py`
50
+ renders the clip. `vendor/` holds four files copied from
51
+ [Chain-of-Zoom](https://github.com/bryanswkim/Chain-of-Zoom) (MIT, see `vendor/COZ-LICENSE`),
52
+ because Spaces do not support git submodules. The only edit to them pins `weights_only=True`
53
+ on three `torch.load` calls, whose default flipped in torch 2.6.
54
+
55
+ ## Honest limits
56
+
57
+ Past the first step or two there is no ground truth left to recover, so the deep levels are
58
+ plausible detail rather than measured detail. The zoom is a crop of your photo, not a real
59
+ lens moving closer. The paper says which claims we do and do not make.
app.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OracleZoom demo: upload a photo, watch it zoom 4x to 256x."""
2
+ import gradio as gr
3
+ import spaces
4
+ from PIL import Image, ImageDraw
5
+
6
+ import geometry
7
+ import video
8
+ import zoom
9
+
10
+ MODELS = zoom.Models()
11
+
12
+ UPSCALE = 4
13
+ ACCENT = "#f5b942"
14
+ LABELS = {1: "input", 4: "4x", 16: "16x", 64: "64x", 256: "256x"}
15
+ SAMPLES = ["0479", "0064", "0245", "0393", "0457"]
16
+
17
+ # Shipped output of a real run, so the page shows the payoff before anyone spends any quota.
18
+ EXAMPLE_CLIP = "samples/example_zoom.mp4"
19
+ EXAMPLE_LEVELS = [("samples/example_1x.png", "input")] + \
20
+ [(f"samples/example_{f}x.png", f"{f}x") for f in (4, 16, 64, 256)]
21
+ EXAMPLE_COMPARE = ("samples/example_256x_input.png", "samples/example_256x.png")
22
+ EXAMPLE_NOTE = "_An example run. Upload a photo above to make your own._"
23
+
24
+ HEADER = """
25
+ <div style="text-align:center;max-width:820px;margin:0 auto 4px">
26
+ <h1 style="margin:0;font-size:2.1em;letter-spacing:-.02em">OracleZoom 🔎</h1>
27
+ <p style="margin:.5em 0 .9em;font-size:1.08em;line-height:1.5;opacity:.85">
28
+ Zoom into any photo far past what it holds. Four steps of 4x take you to
29
+ <b>256x</b>, each one drawn from the last.
30
+ </p>
31
+ <p style="margin:0;font-size:.95em">
32
+ <a href="https://arxiv.org/abs/2609.06490">Paper</a> &nbsp;·&nbsp;
33
+ <a href="https://github.com/dipta007/OracleZoom">Code</a> &nbsp;·&nbsp;
34
+ <a href="https://dipta007.github.io/OracleZoom/">Project page</a> &nbsp;·&nbsp;
35
+ <a href="https://huggingface.co/dipta007/OracleZoom">Model</a>
36
+ </p>
37
+ </div>
38
+ """
39
+
40
+ HOW = """
41
+ ##### How it goes
42
+
43
+ 1. Crop your photo to a 512 square.
44
+ 2. Zoom 4x into the amber box. That crop is blurry, so a vision language model describes it.
45
+ 3. Super-resolve it with that description as the guide.
46
+ 4. Repeat on the result. Four rounds reach 256x.
47
+
48
+ Runs free on ZeroGPU, so the first zoom after a quiet spell waits for a GPU.
49
+ """
50
+
51
+ FOOTER = """
52
+ ### What you are looking at
53
+
54
+ Standard super-resolution models break down well before 16x. OracleZoom gets to 256x by
55
+ zooming one 4x step at a time and feeding each result into the next step. At every step a
56
+ vision language model writes a short description of the crop, and that description guides
57
+ the detail the super-resolution model draws.
58
+
59
+ Past the first step or two there is no ground truth to recover, so the deep levels are
60
+ **plausible detail, not measured detail**. The zoom point is a crop of your photo, not a
61
+ real camera lens moving closer. Read the paper for what we do and do not claim.
62
+
63
+ ```bibtex
64
+ @inproceedings{dipta2027oraclezoom,
65
+ title = {OracleZoom: Reference-Constrained Recursive Super-Resolution},
66
+ author = {Roy Dipta, Shubhashis and Saha, Sourajit and Saha, Shaswati and Sarwar, Nobin},
67
+ year = {2027}
68
+ }
69
+ ```
70
+ """
71
+
72
+ CSS = """
73
+ #hero video {border-radius:12px}
74
+ .contain {max-width:1400px !important}
75
+ footer {display:none !important}
76
+ """
77
+
78
+
79
+ def preview(image, levels, cx, cy):
80
+ """Show where the zoom will go, so nobody spends a run to find out."""
81
+ if image is None:
82
+ return None
83
+ canvas = geometry.resize_and_center_crop(image).convert("RGB")
84
+ rects = geometry.nested_rects(canvas.size, int(levels), UPSCALE, (cx, cy))
85
+ dimmed = Image.blend(canvas, Image.new("RGB", canvas.size, (0, 0, 0)), 0.45)
86
+ dimmed.paste(canvas.crop(rects[0]), rects[0][:2])
87
+ draw = ImageDraw.Draw(dimmed)
88
+ for rect in rects:
89
+ if rect[2] - rect[0] >= 3:
90
+ draw.rectangle(rect, outline=ACCENT, width=2)
91
+ return dimmed
92
+
93
+
94
+ def pick_point(evt: gr.SelectData):
95
+ x, y = evt.index
96
+ return x / geometry.PROCESS_SIZE, y / geometry.PROCESS_SIZE
97
+
98
+
99
+ def estimate_duration(image, levels, cx, cy):
100
+ # Checked against the visitor's remaining quota BEFORE the run, so a loose number locks
101
+ # out low-quota visitors, and a tight one gets the run killed mid-way. Provisional until
102
+ # measured on the real hardware.
103
+ return int(15 + 9 * int(levels))
104
+
105
+
106
+ def _stream(image, levels, cx, cy):
107
+ if image is None:
108
+ raise gr.Error("Upload a photo first.")
109
+ levels = int(levels)
110
+ gallery, prompts, frames, compare = [], [], [], None
111
+ for step, factor, prompt, blurry, result in zoom.zoom(MODELS, image, levels, UPSCALE, (cx, cy)):
112
+ frames.append(result)
113
+ gallery.append((result, LABELS.get(factor, f"{factor}x")))
114
+ if step:
115
+ compare = (blurry, result)
116
+ prompts.append(f"**{LABELS[factor]}** &nbsp; {prompt or '_(no prompt)_'}")
117
+ left = levels - step
118
+ if not step:
119
+ status = f"Warmed up. Zooming {levels} step{'s' if levels > 1 else ''}…"
120
+ elif left:
121
+ status = f"At **{LABELS[factor]}**, {left} step{'s' if left > 1 else ''} to go…"
122
+ else:
123
+ status = f"At **{LABELS[factor]}**. Rendering the clip…"
124
+ # First yield clears any clip left from the previous run; later ones leave it alone.
125
+ yield status, (None if not step else gr.skip()), gallery, compare, "\n\n".join(prompts)
126
+ yield ("Done. 🔎", video.render(frames, UPSCALE, (cx, cy)), gallery, compare,
127
+ "\n\n".join(prompts))
128
+
129
+
130
+ @spaces.GPU(duration=estimate_duration)
131
+ def run(image, levels, cx, cy):
132
+ yield from _stream(image, levels, cx, cy)
133
+
134
+
135
+ @spaces.GPU(duration=estimate_duration(None, 4, 0, 0))
136
+ def run_example(image):
137
+ """One input, so the examples render as thumbnails instead of a four-column table.
138
+ Its results are cached, which is how a visitor with no quota left still sees output."""
139
+ yield from _stream(image, 4, 0.5, 0.5)
140
+
141
+
142
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="amber"), css=CSS,
143
+ title="OracleZoom: zoom to 256x") as demo:
144
+ gr.HTML(HEADER)
145
+ with gr.Row():
146
+ with gr.Column(scale=4):
147
+ image = gr.Image(label="Your photo", type="pil", height=300, sources=["upload", "clipboard"])
148
+ # Seeded with the example so the box overlay explains itself before any upload.
149
+ target = gr.Image(value=preview(Image.open(EXAMPLE_LEVELS[0][0]), 4, 0.5, 0.5),
150
+ label="Click to move the zoom point", type="pil",
151
+ interactive=False, height=340, show_download_button=False)
152
+ levels = gr.Slider(1, 4, value=4, step=1, label="Zoom steps",
153
+ info="1 step = 4x, 4 steps = 256x. Fewer steps finish sooner.")
154
+ with gr.Accordion("Set the point by hand", open=False):
155
+ cx = gr.Slider(0, 1, value=0.5, step=0.01, label="Horizontal")
156
+ cy = gr.Slider(0, 1, value=0.5, step=0.01, label="Vertical")
157
+ go = gr.Button("🔎 Zoom in", variant="primary", size="lg")
158
+ gr.Markdown(HOW)
159
+ with gr.Column(scale=6):
160
+ status = gr.Markdown("Upload a photo, then press **Zoom in**.")
161
+ clip = gr.Video(value=EXAMPLE_CLIP, label="The zoom", elem_id="hero", autoplay=True,
162
+ loop=True, show_share_button=True, height=400)
163
+ gallery = gr.Gallery(value=EXAMPLE_LEVELS, label="Every level", columns=5, height=175,
164
+ object_fit="cover", show_download_button=True)
165
+ compare = gr.ImageSlider(value=EXAMPLE_COMPARE, height=400,
166
+ label="256x: plain enlargement (left) vs OracleZoom (right)")
167
+ gr.Markdown("##### What the model said it saw, step by step")
168
+ prompts = gr.Markdown(EXAMPLE_NOTE)
169
+
170
+ controls = [image, levels, cx, cy]
171
+ for c in controls:
172
+ c.change(preview, controls, target, show_api=False)
173
+ target.select(pick_point, None, [cx, cy], show_api=False)
174
+ go.click(run, controls, [status, clip, gallery, compare, prompts])
175
+
176
+ gr.Examples(
177
+ examples=[f"samples/{n}.png" for n in SAMPLES],
178
+ inputs=[image],
179
+ outputs=[status, clip, gallery, compare, prompts],
180
+ fn=run_example,
181
+ cache_examples=True,
182
+ label="Or try one of these (already computed, costs you nothing)",
183
+ )
184
+ gr.Markdown(FOOTER)
185
+
186
+ if __name__ == "__main__":
187
+ demo.queue(max_size=20).launch()
geometry.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Crop geometry for the zoom. Pure PIL, no model code, so it stays cheap to test."""
2
+ from PIL import Image
3
+
4
+ PROCESS_SIZE = 512
5
+
6
+
7
+ def resize_and_center_crop(img, size=PROCESS_SIZE):
8
+ """CoZ's entry crop: shortest side to `size`, then the centre square."""
9
+ w, h = img.size
10
+ scale = size / min(w, h)
11
+ nw, nh = int(w * scale), int(h * scale)
12
+ img = img.resize((nw, nh), Image.LANCZOS)
13
+ left, top = (nw - size) // 2, (nh - size) // 2
14
+ return img.crop((left, top, left + size, top + size))
15
+
16
+
17
+ def window_rect(size, upscale, center=(0.5, 0.5)):
18
+ """The next level's field of view: a 1/upscale square around `center`, clamped inside."""
19
+ w, h = size
20
+ nw, nh = w // upscale, h // upscale
21
+ left = min(max(int(center[0] * w - nw / 2), 0), w - nw)
22
+ top = min(max(int(center[1] * h - nh / 2), 0), h - nh)
23
+ return left, top, left + nw, top + nh
24
+
25
+
26
+ def zoom_window(img, upscale, center=(0.5, 0.5)):
27
+ return img.crop(window_rect(img.size, upscale, center))
28
+
29
+
30
+ def nested_rects(size, levels, upscale=4, center=(0.5, 0.5)):
31
+ """Where each zoom step lands, in the coordinates of the first canvas."""
32
+ rects, rect = [], (0, 0, size[0], size[1])
33
+ for _ in range(levels):
34
+ side = rect[2] - rect[0]
35
+ l, t, r, b = window_rect((side, side), upscale, center)
36
+ rect = (rect[0] + l, rect[1] + t, rect[0] + r, rect[1] + b)
37
+ rects.append(rect)
38
+ return rects
requirements.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ZeroGPU only runs torch 2.8.0 and newer, which is why these pins differ from the repo's
2
+ # pyproject.toml. diffusers stays at the paper's version: the SR LoRA is injected by matching
3
+ # the class name "AdaLayerNormZero", so a diffusers rename would silently inject nothing.
4
+ # `spaces` is deliberately absent. The platform pins its own copy and a second pin breaks pip.
5
+ torch==2.8.0
6
+ torchvision==0.23.0
7
+ diffusers==0.32.2
8
+ transformers==4.49.0
9
+ accelerate
10
+ peft>=0.13
11
+ safetensors
12
+ huggingface-hub
13
+ qwen-vl-utils
14
+ lpips
15
+ einops
16
+ pyyaml
17
+ tqdm
18
+ numpy
19
+ pillow
20
+ imageio
21
+ imageio-ffmpeg
samples/0064.png ADDED

Git LFS Details

  • SHA256: c554c83f0c1934a856e60f09cd06375f07e54b1fbf30225ae2fe362f99be5830
  • Pointer size: 131 Bytes
  • Size of remote file: 404 kB
samples/0245.png ADDED

Git LFS Details

  • SHA256: e3f8ac0ccaded03df8cb9934aee5cedc9ef2784fa1a16e6e18472a86fc6edb36
  • Pointer size: 131 Bytes
  • Size of remote file: 606 kB
samples/0393.png ADDED

Git LFS Details

  • SHA256: 3fc6957d9ad816ba5e9411eb8a85aab5e77f1676ba0e04062e77571f7939a9a6
  • Pointer size: 131 Bytes
  • Size of remote file: 404 kB
samples/0457.png ADDED

Git LFS Details

  • SHA256: c2ca4b7bf6c3a5c05347ed21ddc9400e766b0b2fe68bc2af9f339b44f8d2e7fb
  • Pointer size: 131 Bytes
  • Size of remote file: 444 kB
samples/0479.png ADDED

Git LFS Details

  • SHA256: 7fbcfaed5bfb6940caa6d89ce10cb401bac79313d7eb462da038bf6e5e9ac292
  • Pointer size: 131 Bytes
  • Size of remote file: 521 kB
samples/example_16x.png ADDED

Git LFS Details

  • SHA256: 893ff445136b2809733703215f251b5e3074cb8e9ddda82951ad044c00928bc0
  • Pointer size: 131 Bytes
  • Size of remote file: 420 kB
samples/example_1x.png ADDED

Git LFS Details

  • SHA256: 1385c20e0a8c4d157ea5df4e3c9fbb54cf3a1582d69151241dad018eb16c3a05
  • Pointer size: 131 Bytes
  • Size of remote file: 488 kB
samples/example_256x.png ADDED

Git LFS Details

  • SHA256: cfa288e72846eed9a74304fe54ce569fc149d910ea185d6c0210d5592f9c9844
  • Pointer size: 131 Bytes
  • Size of remote file: 362 kB
samples/example_256x_input.png ADDED

Git LFS Details

  • SHA256: c4180ea285ecf61b1618029e8373f5b2c5efe40403e71baa796d58ba982f435d
  • Pointer size: 131 Bytes
  • Size of remote file: 205 kB
samples/example_4x.png ADDED

Git LFS Details

  • SHA256: 3326ff364d47ed6beef569f1073c4b42b8ab50a9ac42c9920eb54bf4752b0d8d
  • Pointer size: 131 Bytes
  • Size of remote file: 418 kB
samples/example_64x.png ADDED

Git LFS Details

  • SHA256: e6129a5d9a222aae938a3fc639b63e7039b8e5ae9c2561fd8d93ba6128b614f3
  • Pointer size: 131 Bytes
  • Size of remote file: 406 kB
samples/example_zoom.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dc9b6172522eb830313cf1f18f9bb852706886005850dffd80e45a66f91391c7
3
+ size 3691325
test_app_smoke.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build the whole Gradio graph with the models stubbed out.
2
+
3
+ Catches component, parameter and event-wiring mistakes without a GPU, which is most of what
4
+ breaks a Space on first deploy. The model path itself is not covered here.
5
+
6
+ uv run --no-project --python 3.12 --with pytest --with "gradio==5.50.0" --with spaces \
7
+ --with pillow --with numpy --with imageio --with imageio-ffmpeg \
8
+ pytest space/test_app_smoke.py -q
9
+ """
10
+ import os
11
+ import sys
12
+ import types
13
+
14
+ import numpy as np
15
+ import pytest
16
+ from PIL import Image
17
+
18
+
19
+ @pytest.fixture(scope="module")
20
+ def app():
21
+ """Import app.py with `zoom` replaced by a stub that skips the GPU work."""
22
+ import geometry
23
+
24
+ stub = types.ModuleType("zoom")
25
+ stub.REPO = "dipta007/OracleZoom"
26
+ stub.Models = type("Models", (), {"__init__": lambda self, weights=None: None})
27
+
28
+ def fake_zoom(models, image, levels=4, upscale=4, center=(0.5, 0.5)):
29
+ cur = geometry.resize_and_center_crop(image)
30
+ yield 0, 1, "", cur, cur
31
+ for i in range(levels):
32
+ blurry = geometry.zoom_window(cur, upscale, center).resize(cur.size, Image.BICUBIC)
33
+ yield i + 1, upscale ** (i + 1), f"tag {i}", blurry, blurry
34
+ cur = blurry
35
+
36
+ stub.zoom = fake_zoom
37
+ sys.modules["zoom"] = stub
38
+ import app as module
39
+ return module
40
+
41
+
42
+ @pytest.fixture
43
+ def photo():
44
+ rng = np.random.default_rng(0)
45
+ return Image.fromarray(rng.integers(0, 255, (700, 900, 3), dtype=np.uint8))
46
+
47
+
48
+ def test_the_interface_builds(app):
49
+ assert app.demo.blocks and app.demo.fns
50
+
51
+
52
+ def test_preview_returns_a_canvas_and_tolerates_no_image(app, photo):
53
+ assert app.preview(photo, 4, 0.3, 0.7).size == (512, 512)
54
+ assert app.preview(None, 4, 0.5, 0.5) is None
55
+
56
+
57
+ def test_declared_duration_grows_with_depth(app, photo):
58
+ got = [app.estimate_duration(photo, n, 0.5, 0.5) for n in (1, 2, 3, 4)]
59
+ assert got == sorted(got) and all(isinstance(v, int) for v in got)
60
+
61
+
62
+ def test_the_shipped_example_assets_exist(app):
63
+ for path in [app.EXAMPLE_CLIP, *app.EXAMPLE_COMPARE, *(p for p, _ in app.EXAMPLE_LEVELS)]:
64
+ assert os.path.getsize(path) > 1000, path
65
+ assert [label for _, label in app.EXAMPLE_LEVELS] == ["input", "4x", "16x", "64x", "256x"]
66
+
67
+
68
+ def test_every_sample_named_for_the_examples_is_present(app):
69
+ for name in app.SAMPLES:
70
+ assert os.path.exists(f"samples/{name}.png")
71
+
72
+
73
+ def test_run_streams_one_update_per_level_then_the_clip(app, photo):
74
+ yields = list(app.run(photo, 3, 0.5, 0.5))
75
+ assert len(yields) == 5 # entry level, 3 zoom steps, then the clip
76
+ assert all(len(y) == 5 for y in yields)
77
+ assert yields[0][1] is None # first yield clears any stale clip
78
+ status, clip, gallery, compare, prompts = yields[-1]
79
+ assert [label for _, label in gallery] == ["input", "4x", "16x", "64x"]
80
+ assert compare is not None and len(compare) == 2
81
+ assert prompts.count("tag") == 3
82
+ try:
83
+ assert os.path.getsize(clip) > 10_000
84
+ finally:
85
+ os.remove(clip)
86
+
87
+
88
+ def test_run_rejects_an_empty_upload(app):
89
+ import gradio as gr
90
+ with pytest.raises(gr.Error):
91
+ next(app.run(None, 4, 0.5, 0.5))
test_space.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the parts of the Space that need no GPU: crop geometry and the video render.
2
+
3
+ uv run --with pytest --with pillow --with numpy --with imageio --with imageio-ffmpeg \
4
+ pytest space/test_space.py -q
5
+ """
6
+ import os
7
+
8
+ import numpy as np
9
+ import pytest
10
+ from PIL import Image
11
+
12
+ import geometry
13
+ import video
14
+
15
+
16
+ def _img(seed, size=512):
17
+ rng = np.random.default_rng(seed)
18
+ return Image.fromarray(rng.integers(0, 255, (size, size, 3), dtype=np.uint8))
19
+
20
+
21
+ def test_entry_crop_is_square_and_centred():
22
+ out = geometry.resize_and_center_crop(_img(0, 900).resize((1200, 900)))
23
+ assert out.size == (512, 512)
24
+
25
+
26
+ def test_window_is_one_quarter_wide_and_centred_by_default():
27
+ assert geometry.window_rect((512, 512), 4) == (192, 192, 320, 320)
28
+
29
+
30
+ def test_window_stays_inside_the_frame_at_the_edges():
31
+ for center in [(0.0, 0.0), (1.0, 1.0), (0.0, 1.0), (1.0, 0.0)]:
32
+ l, t, r, b = geometry.window_rect((512, 512), 4, center)
33
+ assert 0 <= l < r <= 512 and 0 <= t < b <= 512
34
+ assert (r - l, b - t) == (128, 128)
35
+
36
+
37
+ def test_nested_rects_shrink_by_upscale_and_nest():
38
+ rects = geometry.nested_rects((512, 512), 4, 4)
39
+ assert [r[2] - r[0] for r in rects] == [128, 32, 8, 2]
40
+ for inner, outer in zip(rects[1:], rects):
41
+ assert outer[0] <= inner[0] and inner[2] <= outer[2]
42
+ assert outer[1] <= inner[1] and inner[3] <= outer[3]
43
+
44
+
45
+ def test_zoom_window_matches_window_rect():
46
+ img = _img(1)
47
+ assert geometry.zoom_window(img, 4).size == (128, 128)
48
+ assert np.array_equal(np.asarray(geometry.zoom_window(img, 4)),
49
+ np.asarray(img.crop(geometry.window_rect(img.size, 4))))
50
+
51
+
52
+ def test_video_needs_two_levels():
53
+ assert video.render([_img(2)]) is None
54
+
55
+
56
+ def test_video_renders_an_mp4_with_the_expected_frame_count():
57
+ levels = [_img(i) for i in range(5)]
58
+ path = video.render(levels, 4, (0.5, 0.5))
59
+ try:
60
+ assert os.path.getsize(path) > 10_000
61
+ import imageio.v2 as imageio
62
+ frames = imageio.mimread(path, memtest=False)
63
+ expected = 4 * (video.PUSH_FRAMES + video.FADE_FRAMES) + video.TAIL_FRAMES
64
+ assert len(frames) == expected
65
+ assert frames[0].shape[:2] == (512, 512)
66
+ finally:
67
+ os.remove(path)
68
+
69
+
70
+ def test_the_push_ends_exactly_on_the_next_level_input():
71
+ """The last push frame must equal the blurry crop the next level was built from.
72
+ If it does not, the clip jumps at every transition."""
73
+ levels = [_img(3), _img(4)]
74
+ frames = list(video._frames(levels, 4, (0.5, 0.5)))
75
+ last_push = np.asarray(frames[video.PUSH_FRAMES - 1], dtype=np.int16)
76
+ target = geometry.window_rect(levels[0].size, 4, (0.5, 0.5))
77
+ blurry = np.asarray(levels[0].crop(target).resize(levels[0].size, Image.BICUBIC),
78
+ dtype=np.int16)
79
+ assert np.array_equal(last_push, blurry)
80
+
81
+
82
+ def test_the_fade_ends_on_the_sharp_level():
83
+ levels = [_img(5), _img(6)]
84
+ frames = list(video._frames(levels, 4, (0.5, 0.5)))
85
+ end_of_fade = np.asarray(frames[video.PUSH_FRAMES + video.FADE_FRAMES - 1], dtype=np.int16)
86
+ assert np.abs(end_of_fade - np.asarray(levels[1], dtype=np.int16)).max() <= 1
87
+
88
+
89
+ if __name__ == "__main__":
90
+ raise SystemExit(pytest.main([__file__, "-q"]))
vendor/COZ-LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Bryan Sangwoo Kim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
vendor/lora/lora_layers.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional, Set, Type, Union
2
+
3
+ import torch
4
+ from torch import nn
5
+
6
+
7
+ class LoraInjectedLinear(nn.Module):
8
+ """
9
+ Linear layer with LoRA injection.
10
+ Taken from https://github.com/cloneofsimo/lora/blob/master/lora_diffusion/lora.py
11
+ """
12
+ def __init__(
13
+ self, in_features, out_features, bias=False, r=4, dropout_p=0.1, scale=1.0
14
+ ):
15
+ super().__init__()
16
+
17
+ if r > min(in_features, out_features):
18
+ raise ValueError(
19
+ f"LoRA rank {r} must be less or equal than {min(in_features, out_features)}"
20
+ )
21
+ self.r = r
22
+ self.linear = nn.Linear(in_features, out_features, bias)
23
+ self.lora_down = nn.Linear(in_features, r, bias=False)
24
+ self.dropout = nn.Dropout(dropout_p)
25
+ self.lora_up = nn.Linear(r, out_features, bias=False)
26
+ self.scale = scale
27
+ self.selector = nn.Identity()
28
+
29
+ nn.init.normal_(self.lora_down.weight, std=1 / r)
30
+ nn.init.zeros_(self.lora_up.weight)
31
+
32
+ def forward(self, input):
33
+ return (
34
+ self.linear(input.float())
35
+ + self.dropout(self.lora_up(self.selector(self.lora_down(input.float()))))
36
+ * self.scale
37
+ ).half()
38
+
39
+ def realize_as_lora(self):
40
+ return self.lora_up.weight.data * self.scale, self.lora_down.weight.data
41
+
42
+ def set_selector_from_diag(self, diag: torch.Tensor):
43
+ # diag is a 1D tensor of size (r,)
44
+ assert diag.shape == (self.r,)
45
+ self.selector = nn.Linear(self.r, self.r, bias=False)
46
+ self.selector.weight.data = torch.diag(diag)
47
+ self.selector.weight.data = self.selector.weight.data.to(
48
+ self.lora_up.weight.device
49
+ ).to(self.lora_up.weight.dtype)
50
+
51
+ class LoraInjectedConv2d(nn.Module):
52
+ def __init__(
53
+ self,
54
+ in_channels: int,
55
+ out_channels: int,
56
+ kernel_size,
57
+ stride=1,
58
+ padding=0,
59
+ dilation=1,
60
+ groups: int = 1,
61
+ bias: bool = True,
62
+ r: int = 4,
63
+ dropout_p: float = 0.1,
64
+ scale: float = 1.0,
65
+ ):
66
+ super().__init__()
67
+ if r > min(in_channels, out_channels):
68
+ raise ValueError(
69
+ f"LoRA rank {r} must be less or equal than {min(in_channels, out_channels)}"
70
+ )
71
+ self.r = r
72
+ self.conv = nn.Conv2d(
73
+ in_channels=in_channels,
74
+ out_channels=out_channels,
75
+ kernel_size=kernel_size,
76
+ stride=stride,
77
+ padding=padding,
78
+ dilation=dilation,
79
+ groups=groups,
80
+ bias=bias,
81
+ )
82
+
83
+ self.lora_down = nn.Conv2d(
84
+ in_channels=in_channels,
85
+ out_channels=r,
86
+ kernel_size=kernel_size,
87
+ stride=stride,
88
+ padding=padding,
89
+ dilation=dilation,
90
+ groups=groups,
91
+ bias=False,
92
+ )
93
+ self.dropout = nn.Dropout(dropout_p)
94
+ self.lora_up = nn.Conv2d(
95
+ in_channels=r,
96
+ out_channels=out_channels,
97
+ kernel_size=1,
98
+ stride=1,
99
+ padding=0,
100
+ bias=False,
101
+ )
102
+ self.selector = nn.Identity()
103
+ self.scale = scale
104
+
105
+ nn.init.normal_(self.lora_down.weight, std=1 / r)
106
+ nn.init.zeros_(self.lora_up.weight)
107
+
108
+ def forward(self, input):
109
+ return (
110
+ self.conv(input)
111
+ + self.dropout(self.lora_up(self.selector(self.lora_down(input))))
112
+ * self.scale
113
+ )
114
+
115
+ def realize_as_lora(self):
116
+ return self.lora_up.weight.data * self.scale, self.lora_down.weight.data
117
+
118
+ def set_selector_from_diag(self, diag: torch.Tensor):
119
+ # diag is a 1D tensor of size (r,)
120
+ assert diag.shape == (self.r,)
121
+ self.selector = nn.Conv2d(
122
+ in_channels=self.r,
123
+ out_channels=self.r,
124
+ kernel_size=1,
125
+ stride=1,
126
+ padding=0,
127
+ bias=False,
128
+ )
129
+ self.selector.weight.data = torch.diag(diag)
130
+
131
+ # same device + dtype as lora_up
132
+ self.selector.weight.data = self.selector.weight.data.to(
133
+ self.lora_up.weight.device
134
+ ).to(self.lora_up.weight.dtype)
135
+
136
+
137
+
vendor/lora/lora_utils.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+ from lora.lora_layers import LoraInjectedLinear, LoraInjectedConv2d
4
+
5
+ def _find_modules(model, ancestor_class=None, search_class=[nn.Linear], exclude_children_of=[LoraInjectedLinear]):
6
+ # Get the targets we should replace all linears under
7
+ if ancestor_class is not None:
8
+ ancestors = (
9
+ module
10
+ for module in model.modules()
11
+ if module.__class__.__name__ in ancestor_class
12
+ )
13
+ else:
14
+ # this, incase you want to naively iterate over all modules.
15
+ ancestors = [module for module in model.modules()]
16
+
17
+ for ancestor in ancestors:
18
+ for fullname, module in ancestor.named_modules():
19
+ # if 'norm1_context' in fullname:
20
+ if any([isinstance(module, _class) for _class in search_class]):
21
+ *path, name = fullname.split(".")
22
+ parent = ancestor
23
+ while path:
24
+ parent = parent.get_submodule(path.pop(0))
25
+ if exclude_children_of and any(
26
+ [isinstance(parent, _class) for _class in exclude_children_of]
27
+ ):
28
+ continue
29
+ yield parent, name, module
30
+
31
+ def extract_lora_ups_down(model, target_replace_module={'AdaLayerNormZero'}): # Attention for kv_lora
32
+
33
+ loras = []
34
+
35
+ for _m, _n, _child_module in _find_modules(
36
+ model,
37
+ target_replace_module,
38
+ search_class=[LoraInjectedLinear, LoraInjectedConv2d],
39
+ ):
40
+ loras.append((_child_module.lora_up, _child_module.lora_down))
41
+
42
+ if len(loras) == 0:
43
+ raise ValueError("No lora injected.")
44
+
45
+ return loras
46
+
47
+ def save_lora_weight(
48
+ model,
49
+ path="./lora.pt",
50
+ target_replace_module={'AdaLayerNormZero'}, # Attention for kv_lora
51
+ save_half:bool=False
52
+ ):
53
+ weights = []
54
+ for _up, _down in extract_lora_ups_down(
55
+ model, target_replace_module=target_replace_module
56
+ ):
57
+ dtype = torch.float16 if save_half else torch.float32
58
+ weights.append(_up.weight.to("cpu").to(dtype))
59
+ weights.append(_down.weight.to("cpu").to(dtype))
60
+
61
+ torch.save(weights, path)
vendor/osediff_sd3.py ADDED
@@ -0,0 +1,913 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ sys.path.append(os.getcwd())
4
+ import yaml
5
+ import copy
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
+ from typing import List, Tuple, Optional
10
+ import numpy as np
11
+ import lpips
12
+ from torchvision import transforms
13
+ from PIL import Image
14
+ from peft import LoraConfig, get_peft_model
15
+
16
+ from copy import deepcopy
17
+ from tqdm import tqdm
18
+
19
+ from diffusers import StableDiffusion3Pipeline
20
+ from lora.lora_layers import LoraInjectedLinear, LoraInjectedConv2d
21
+
22
+ from utils.vaehook import VAEHook
23
+
24
+
25
+ def inject_lora_vae(vae, lora_rank=4, init_lora_weights="gaussian", verbose=False):
26
+ """
27
+ Inject LoRA into the VAE's encoder
28
+ """
29
+ vae.requires_grad_(False)
30
+ vae.train()
31
+
32
+ # Identify modules to LoRA-ify in the encoder
33
+ l_grep = ["conv1", "conv2", "conv_in", "conv_shortcut",
34
+ "conv", "conv_out", "to_k", "to_q", "to_v", "to_out.0"]
35
+ l_target_modules_encoder = []
36
+ for n, p in vae.named_parameters():
37
+ if "bias" in n or "norm" in n:
38
+ continue
39
+ for pattern in l_grep:
40
+ if (pattern in n) and ("encoder" in n):
41
+ l_target_modules_encoder.append(n.replace(".weight", ""))
42
+ elif ("quant_conv" in n) and ("post_quant_conv" not in n):
43
+ l_target_modules_encoder.append(n.replace(".weight", ""))
44
+
45
+ if verbose:
46
+ print("The following VAE parameters will get LoRA:")
47
+ print(l_target_modules_encoder)
48
+
49
+ # Create and add a LoRA adapter
50
+ lora_conf_encoder = LoraConfig(
51
+ r=lora_rank,
52
+ init_lora_weights=init_lora_weights,
53
+ target_modules=l_target_modules_encoder
54
+ )
55
+
56
+ adapter_name = "default_encoder"
57
+ try:
58
+ vae.add_adapter(lora_conf_encoder, adapter_name=adapter_name)
59
+ vae.set_adapter(adapter_name)
60
+ except ValueError as e:
61
+ if "already exists" in str(e):
62
+ print(f"Adapter with name {adapter_name} already exists. Skipping injection.")
63
+ else:
64
+ raise e
65
+
66
+ return vae, l_target_modules_encoder
67
+
68
+ def _find_modules(model, ancestor_class=None, search_class=[nn.Linear], exclude_children_of=[LoraInjectedLinear]):
69
+ # Get the targets we should replace all linears under
70
+ if ancestor_class is not None:
71
+ ancestors = (
72
+ module
73
+ for module in model.modules()
74
+ if module.__class__.__name__ in ancestor_class
75
+ )
76
+ else:
77
+ # this, in case you want to naively iterate over all modules.
78
+ ancestors = [module for module in model.modules()]
79
+
80
+ for ancestor in ancestors:
81
+ for fullname, module in ancestor.named_modules():
82
+ if any([isinstance(module, _class) for _class in search_class]):
83
+ *path, name = fullname.split(".")
84
+ parent = ancestor
85
+ while path:
86
+ parent = parent.get_submodule(path.pop(0))
87
+ if exclude_children_of and any(
88
+ [isinstance(parent, _class) for _class in exclude_children_of]
89
+ ):
90
+ continue
91
+ yield parent, name, module
92
+
93
+ def inject_lora(model, ancestor_class, loras=None, r:int=4, dropout_p:float=0.0, scale:float=1.0, verbose:bool=False):
94
+
95
+ model.requires_grad_(False)
96
+ model.train()
97
+
98
+ names = []
99
+ require_grad_params = [] # to be updated
100
+
101
+ total_lora_params = 0
102
+
103
+ if loras is not None:
104
+ loras = torch.load(loras, map_location=model.device, weights_only=True)
105
+ loras = [lora.float() for lora in loras]
106
+
107
+ for _module, name, _child_module in _find_modules(model, ancestor_class): # SiLU + Linear Block
108
+ weight = _child_module.weight
109
+ bias = _child_module.bias
110
+
111
+ if verbose:
112
+ print(f'LoRA Injection : injecting lora into {name}')
113
+
114
+ _tmp = LoraInjectedLinear(
115
+ _child_module.in_features,
116
+ _child_module.out_features,
117
+ _child_module.bias is not None,
118
+ r=r,
119
+ dropout_p=dropout_p,
120
+ scale=scale,
121
+ )
122
+ _tmp.linear.weight = nn.Parameter(weight.float())
123
+ if bias is not None:
124
+ _tmp.linear.bias = nn.Parameter(bias.float())
125
+
126
+ # switch the module
127
+ _tmp.to(device=_child_module.weight.device, dtype=torch.float) # keep as float / mixed precision
128
+ _module._modules[name] = _tmp
129
+
130
+ require_grad_params.append(_module._modules[name].lora_up.parameters())
131
+ require_grad_params.append(_module._modules[name].lora_down.parameters())
132
+
133
+ if loras != None:
134
+ _module._modules[name].lora_up.weight = nn.Parameter(loras.pop(0))
135
+ _module._modules[name].lora_down.weight = nn.Parameter(loras.pop(0))
136
+
137
+ _module._modules[name].lora_up.weight.requires_grad = True
138
+ _module._modules[name].lora_down.weight.requires_grad = True
139
+ names.append(name)
140
+
141
+ if verbose:
142
+ # -------- Count LoRA parameters just added --------
143
+ lora_up_count = sum(p.numel() for p in _tmp.lora_up.parameters())
144
+ lora_down_count = sum(p.numel() for p in _tmp.lora_down.parameters())
145
+ lora_total_for_this_layer = lora_up_count + lora_down_count
146
+ total_lora_params += lora_total_for_this_layer
147
+ print(f" Added {lora_total_for_this_layer} params "
148
+ f"(lora_up={lora_up_count}, lora_down={lora_down_count})")
149
+
150
+ if verbose:
151
+ print(f"Total new LoRA parameters added: {total_lora_params}")
152
+
153
+ return require_grad_params, names
154
+
155
+ def add_mp_hook(transformer):
156
+ '''
157
+ For mixed precision of LoRA. (i.e. keep LoRA as float and others as half)
158
+ '''
159
+ def pre_hook(module, input):
160
+ return input.float()
161
+
162
+ def post_hook(module, input, output):
163
+ return output.half()
164
+
165
+ hooks = []
166
+ for _module, name, _child_module in _find_modules(transformer):
167
+ if isinstance(_child_module, LoraInjectedLinear):
168
+ hook = _child_module.lora_up.register_forward_pre_hook(pre_hook)
169
+ hooks.append(hook)
170
+ hook = _child_module.lora_down.register_forward_hook(post_hook)
171
+ hooks.append(hook)
172
+
173
+ return transformer, hooks
174
+
175
+ def compute_density_for_timestep_sampling(
176
+ weighting_scheme: str, batch_size: int, logit_mean: float = 0.0, logit_std: float = 1.0, mode_scale: Optional[float] = None
177
+ ):
178
+ """
179
+ Compute the density for sampling the timesteps when doing SD3 training.
180
+
181
+ Courtesy: This was contributed by Rafie Walker in https://github.com/huggingface/diffusers/pull/8528.
182
+
183
+ SD3 paper reference: https://arxiv.org/abs/2403.03206v1.
184
+ """
185
+ if weighting_scheme == "logit_normal":
186
+ # See 3.1 in the SD3 paper ($rf/lognorm(0.00,1.00)$).
187
+ u = torch.normal(mean=logit_mean, std=logit_std, size=(batch_size,), device="cpu")
188
+ u = torch.nn.functional.sigmoid(u)
189
+ elif weighting_scheme == "mode":
190
+ u = torch.rand(size=(batch_size,), device="cpu")
191
+ u = 1 - u - mode_scale * (torch.cos(math.pi * u / 2) ** 2 - 1 + u)
192
+ else:
193
+ u = torch.rand(size=(batch_size,), device="cpu")
194
+ return u
195
+
196
+ def compute_loss_weighting_for_sd3(weighting_scheme: str, sigmas):
197
+ """
198
+ Computes loss weighting scheme for SD3 training.
199
+
200
+ Courtesy: This was contributed by Rafie Walker in https://github.com/huggingface/diffusers/pull/8528.
201
+
202
+ SD3 paper reference: https://arxiv.org/abs/2403.03206v1.
203
+ """
204
+ if weighting_scheme == "sigma_sqrt":
205
+ weighting = (sigmas**-2.0).float()
206
+ elif weighting_scheme == "cosmap":
207
+ bot = 1 - 2 * sigmas + 2 * sigmas**2
208
+ weighting = 2 / (math.pi * bot)
209
+ else:
210
+ weighting = torch.ones_like(sigmas)
211
+ return weighting
212
+
213
+
214
+ class StableDiffusion3Base():
215
+ def __init__(self, model_key:str='stabilityai/stable-diffusion-3-medium-diffusers', device='cuda', dtype=torch.float16):
216
+ self.device = device
217
+ self.dtype = dtype
218
+
219
+ pipe = StableDiffusion3Pipeline.from_pretrained(model_key, torch_dtype=self.dtype)
220
+
221
+ self.scheduler = pipe.scheduler
222
+
223
+ self.tokenizer_1 = pipe.tokenizer
224
+ self.tokenizer_2 = pipe.tokenizer_2
225
+ self.tokenizer_3 = pipe.tokenizer_3
226
+ self.text_enc_1 = pipe.text_encoder.to(device)
227
+ self.text_enc_2 = pipe.text_encoder_2.to(device)
228
+ self.text_enc_3 = pipe.text_encoder_3.to(device)
229
+
230
+ self.vae=pipe.vae.to(device)
231
+
232
+ self.transformer = pipe.transformer.to(device)
233
+ self.transformer.eval()
234
+ self.transformer.requires_grad_(False)
235
+
236
+ self.vae_scale_factor = (
237
+ 2 ** (len(self.vae.config.block_out_channels)-1) if hasattr(self, "vae") and self.vae is not None else 8
238
+ )
239
+
240
+ del pipe
241
+
242
+ def encode_prompt(self, prompt: List[str], batch_size:int=1) -> List[torch.Tensor]:
243
+ '''
244
+ We assume that
245
+ 1. number of tokens < max_length
246
+ 2. one prompt for one image
247
+ '''
248
+ # CLIP encode (used for modulation of adaLN-zero)
249
+ # now, we have two CLIPs
250
+ text_clip1_ids = self.tokenizer_1(prompt,
251
+ padding="max_length",
252
+ max_length=77,
253
+ truncation=True,
254
+ return_tensors='pt').input_ids
255
+ text_clip1_emb = self.text_enc_1(text_clip1_ids.to(self.device), output_hidden_states=True)
256
+ pool_clip1_emb = text_clip1_emb[0].to(dtype=self.dtype, device=self.device)
257
+ text_clip1_emb = text_clip1_emb.hidden_states[-2].to(dtype=self.dtype, device=self.device)
258
+
259
+ text_clip2_ids = self.tokenizer_2(prompt,
260
+ padding="max_length",
261
+ max_length=77,
262
+ truncation=True,
263
+ return_tensors='pt').input_ids
264
+ text_clip2_emb = self.text_enc_2(text_clip2_ids.to(self.device), output_hidden_states=True)
265
+ pool_clip2_emb = text_clip2_emb[0].to(dtype=self.dtype, device=self.device)
266
+ text_clip2_emb = text_clip2_emb.hidden_states[-2].to(dtype=self.dtype, device=self.device)
267
+
268
+ # T5 encode (used for text condition)
269
+ text_t5_ids = self.tokenizer_3(prompt,
270
+ padding="max_length",
271
+ max_length=512,
272
+ truncation=True,
273
+ add_special_tokens=True,
274
+ return_tensors='pt').input_ids
275
+ text_t5_emb = self.text_enc_3(text_t5_ids.to(self.device))[0]
276
+ text_t5_emb = text_t5_emb.to(dtype=self.dtype, device=self.device)
277
+
278
+ # Merge
279
+ clip_prompt_emb = torch.cat([text_clip1_emb, text_clip2_emb], dim=-1)
280
+ clip_prompt_emb = torch.nn.functional.pad(
281
+ clip_prompt_emb, (0, text_t5_emb.shape[-1] - clip_prompt_emb.shape[-1])
282
+ )
283
+ prompt_emb = torch.cat([clip_prompt_emb, text_t5_emb], dim=-2)
284
+ pooled_prompt_emb = torch.cat([pool_clip1_emb, pool_clip2_emb], dim=-1)
285
+
286
+ return prompt_emb, pooled_prompt_emb
287
+
288
+ def initialize_latent(self, img_size:Tuple[int], batch_size:int=1, **kwargs):
289
+ H, W = img_size
290
+ lH, lW = H//self.vae_scale_factor, W//self.vae_scale_factor
291
+ lC = self.transformer.config.in_channels
292
+ latent_shape = (batch_size, lC, lH, lW)
293
+
294
+ z = torch.randn(latent_shape, device=self.device, dtype=self.dtype)
295
+
296
+ return z
297
+
298
+ def encode(self, image: torch.Tensor) -> torch.Tensor:
299
+ z = self.vae.encode(image).latent_dist.sample()
300
+ z = (z-self.vae.config.shift_factor) * self.vae.config.scaling_factor
301
+ return z
302
+
303
+ def decode(self, z: torch.Tensor) -> torch.Tensor:
304
+ z = (z/self.vae.config.scaling_factor) + self.vae.config.shift_factor
305
+ return self.vae.decode(z, return_dict=False)[0]
306
+
307
+
308
+ class SD3Euler(StableDiffusion3Base):
309
+ def __init__(self, model_key:str='stabilityai/stable-diffusion-3-medium-diffusers', device='cuda'):
310
+ super().__init__(model_key=model_key, device=device)
311
+
312
+ def inversion(self, src_img, prompts: List[str], NFE:int, cfg_scale: float=1.0, batch_size: int=1):
313
+
314
+ # encode text prompts
315
+ prompt_emb, pooled_emb = self.encode_prompt(prompts, batch_size)
316
+ null_prompt_emb, null_pooled_emb = self.encode_prompt([""], batch_size)
317
+
318
+ # initialize latent
319
+ src_img = src_img.to(device=self.device, dtype=self.dtype)
320
+ with torch.no_grad():
321
+ z = self.encode(src_img)
322
+ z0 = z.clone()
323
+
324
+ # timesteps (default option. You can make your custom here.)
325
+ self.scheduler.set_timesteps(NFE, device=self.device)
326
+ timesteps = self.scheduler.timesteps
327
+ timesteps = torch.cat([timesteps, torch.zeros(1, device=self.device)])
328
+ timesteps = reversed(timesteps)
329
+ sigmas = timesteps / self.scheduler.config.num_train_timesteps
330
+
331
+ # Solve ODE
332
+ pbar = tqdm(timesteps[:-1], total=NFE, desc='SD3 Euler Inversion')
333
+ for i, t in enumerate(pbar):
334
+ timestep = t.expand(z.shape[0]).to(self.device)
335
+ pred_v = self.predict_vector(z, timestep, prompt_emb, pooled_emb)
336
+ if cfg_scale != 1.0:
337
+ pred_null_v = self.predict_vector(z, timestep, null_prompt_emb, null_pooled_emb)
338
+ else:
339
+ pred_null_v = 0.0
340
+
341
+ sigma = sigmas[i]
342
+ sigma_next = sigmas[i+1]
343
+
344
+ z = z + (sigma_next - sigma) * (pred_null_v + cfg_scale * (pred_v - pred_null_v))
345
+
346
+ return z
347
+
348
+ def sample(self, prompts: List[str], NFE:int, img_shape: Optional[Tuple[int]]=None, cfg_scale: float=1.0, batch_size: int = 1, latent:Optional[torch.Tensor]=None):
349
+ imgH, imgW = img_shape if img_shape is not None else (512, 512)
350
+
351
+ # encode text prompts
352
+ with torch.no_grad():
353
+ prompt_emb, pooled_emb = self.encode_prompt(prompts, batch_size)
354
+ null_prompt_emb, null_pooled_emb = self.encode_prompt([""], batch_size)
355
+
356
+ # initialize latent
357
+ if latent is None:
358
+ z = self.initialize_latent((imgH, imgW), batch_size)
359
+ else:
360
+ z = latent
361
+
362
+ # timesteps (default option. You can make your custom here.)
363
+ self.scheduler.set_timesteps(NFE, device=self.device)
364
+ timesteps = self.scheduler.timesteps
365
+ sigmas = timesteps / self.scheduler.config.num_train_timesteps
366
+
367
+ # Solve ODE
368
+ pbar = tqdm(timesteps, total=NFE, desc='SD3 Euler')
369
+ for i, t in enumerate(pbar):
370
+ timestep = t.expand(z.shape[0]).to(self.device)
371
+ pred_v = self.predict_vector(z, timestep, prompt_emb, pooled_emb)
372
+ if cfg_scale != 1.0:
373
+ pred_null_v = self.predict_vector(z, timestep, null_prompt_emb, null_pooled_emb)
374
+ else:
375
+ pred_null_v = 0.0
376
+
377
+ sigma = sigmas[i]
378
+ sigma_next = sigmas[i+1] if i+1 < NFE else 0.0
379
+
380
+ z = z + (sigma_next - sigma) * (pred_null_v + cfg_scale * (pred_v - pred_null_v))
381
+
382
+ # decode
383
+ with torch.no_grad():
384
+ img = self.decode(z)
385
+ return img
386
+
387
+
388
+ class OSEDiff_SD3_GEN(torch.nn.Module):
389
+ def __init__(self, args, base_model):
390
+ super().__init__()
391
+
392
+ self.args = args
393
+ self.model = base_model
394
+
395
+ # Add lora to transformer
396
+ print('Adding LoRA to OSEDiff_SD3_GEN')
397
+ self.transformer_gen = copy.deepcopy(self.model.transformer)
398
+ self.transformer_gen.to('cuda:1')
399
+
400
+ self.transformer_gen.requires_grad_(False)
401
+ self.transformer_gen.train()
402
+ self.transformer_gen, hooks = add_mp_hook(self.transformer_gen)
403
+ self.hooks = hooks
404
+
405
+ lora_params, _ = inject_lora(self.transformer_gen, {"AdaLayerNormZero"}, r=args.lora_rank, verbose=False)
406
+ for name, param in self.transformer_gen.named_parameters():
407
+ if "lora_" in name:
408
+ param.requires_grad = True # LoRA up/down
409
+ else:
410
+ param.requires_grad = False # everything else
411
+
412
+ # Insert LoRA into VAE
413
+ print("Adding LoRA to VAE")
414
+ self.model.vae, self.lora_vae_modules_encoder = inject_lora_vae(self.model.vae, lora_rank=args.lora_rank, verbose=False)
415
+
416
+ def predict_vector(self, z, t, prompt_emb, pooled_emb):
417
+ v = self.transformer_gen(hidden_states=z,
418
+ timestep=t,
419
+ pooled_projections=pooled_emb,
420
+ encoder_hidden_states=prompt_emb,
421
+ return_dict=False)[0]
422
+ return v
423
+
424
+ def forward(self, x_src, batch=None, args=None):
425
+
426
+ z_src = self.model.encode(x_src.to(dtype=torch.float32, device=self.model.vae.device))
427
+ z_src = z_src.to(self.transformer_gen.device)
428
+
429
+ # calculate prompt_embeddings and neg_prompt_embeddings
430
+ batch_size, _, _, _ = x_src.shape
431
+ with torch.no_grad():
432
+ prompt_embeds, pooled_embeds = self.model.encode_prompt(batch["prompt"], batch_size)
433
+ neg_prompt_embeds, neg_pooled_embeds = self.model.encode_prompt(batch["neg_prompt"], batch_size)
434
+
435
+ NFE = 1
436
+ self.model.scheduler.set_timesteps(NFE, device=self.model.device)
437
+ timesteps = self.model.scheduler.timesteps
438
+ sigmas = timesteps / self.model.scheduler.config.num_train_timesteps
439
+ sigmas = sigmas.to(self.transformer_gen.device)
440
+
441
+ # Solve ODE
442
+ i = 0
443
+ t = timesteps[0]
444
+
445
+ timestep = t.expand(z_src.shape[0]).to(self.transformer_gen.device)
446
+ prompt_embeds = prompt_embeds.to(self.transformer_gen.device, dtype=torch.float32)
447
+ pooled_embeds = pooled_embeds.to(self.transformer_gen.device, dtype=torch.float32)
448
+ pred_v = self.predict_vector(z_src, timestep, prompt_embeds, pooled_embeds)
449
+ pred_null_v = 0.0
450
+
451
+ sigma = sigmas[i]
452
+ sigma_next = sigmas[i+1] if i+1 < NFE else 0.0
453
+
454
+ z_src = z_src + (sigma_next - sigma) * (pred_null_v + 1 * (pred_v - pred_null_v))
455
+
456
+ output_image = self.model.decode(z_src.to(dtype=torch.float32, device=self.model.vae.device))
457
+
458
+ return output_image, z_src, prompt_embeds, pooled_embeds
459
+
460
+
461
+ class OSEDiff_SD3_REG(torch.nn.Module):
462
+ def __init__(self, args, base_model):
463
+ super().__init__()
464
+
465
+ self.args = args
466
+ self.model = base_model
467
+ self.transformer_org = self.model.transformer
468
+
469
+ # Add lora to transformer
470
+ print('Adding LoRA to OSEDiff_SD3_REG')
471
+ self.transformer_reg = copy.deepcopy(self.transformer_org)
472
+ self.transformer_reg.to('cuda:1')
473
+
474
+ self.transformer_reg.requires_grad_(False)
475
+ self.transformer_reg.train()
476
+ self.transformer_reg, hooks = add_mp_hook(self.transformer_reg)
477
+ self.hooks = hooks
478
+
479
+ lora_params, _ = inject_lora(self.transformer_reg, {"AdaLayerNormZero"}, r=args.lora_rank, verbose=False)
480
+ for name, param in self.transformer_reg.named_parameters():
481
+ if "lora_" in name:
482
+ param.requires_grad = True # LoRA up/down
483
+ else:
484
+ param.requires_grad = False # everything else
485
+
486
+ def predict_vector_reg(self, z, t, prompt_emb, pooled_emb):
487
+ v = self.transformer_reg(hidden_states=z,
488
+ timestep=t,
489
+ pooled_projections=pooled_emb,
490
+ encoder_hidden_states=prompt_emb,
491
+ return_dict=False)[0]
492
+ return v
493
+
494
+ def predict_vector_org(self, z, t, prompt_emb, pooled_emb):
495
+ v = self.transformer_org(hidden_states=z,
496
+ timestep=t,
497
+ pooled_projections=pooled_emb,
498
+ encoder_hidden_states=prompt_emb,
499
+ return_dict=False)[0]
500
+ return v
501
+
502
+ def distribution_matching_loss(self, z0, prompt_embeds, pooled_embeds, global_step, args):
503
+
504
+ with torch.no_grad():
505
+ device = self.transformer_reg.device
506
+ # get timesteps and sigma
507
+ u = compute_density_for_timestep_sampling(
508
+ weighting_scheme="uniform",
509
+ batch_size=1,
510
+ logit_mean=0.0,
511
+ logit_std=1.0,
512
+ mode_scale=1.29,
513
+ )
514
+
515
+ t_idx = (u*1000).long().to(device)
516
+ self.model.scheduler.set_timesteps(1000, device=device)
517
+ times = self.model.scheduler.timesteps
518
+ t = times[t_idx]
519
+ sigma = t / 1000
520
+
521
+ # get noise and xt
522
+ z0 = z0.to(device)
523
+ noise = torch.randn_like(z0)
524
+ sigma = sigma.half()
525
+ zt = (1-sigma) * z0 + sigma * noise
526
+
527
+ # Get x0_prediction of transformer_reg
528
+ v_pred_reg = self.predict_vector_reg(zt, t, prompt_embeds.to(device), pooled_embeds.to(device))
529
+ reg_model_pred = v_pred_reg * (-sigma) + zt # this is x0_prediction for reg
530
+
531
+ # Get x0_prediction of transformer_org
532
+ org_device = self.transformer_org.device
533
+ v_pred_org = self.predict_vector_org(zt.to(org_device), t.to(org_device), prompt_embeds.to(org_device), pooled_embeds.to(org_device))
534
+ org_model_pred = v_pred_org * (-sigma.to(org_device)) + zt.to(org_device) # this is x0_prediction for org
535
+
536
+ # Visualization
537
+ if global_step % 100 == 1:
538
+ self.vsd_visualization(z0, noise, zt, reg_model_pred, org_model_pred, global_step, args)
539
+
540
+ weighting_factor = torch.abs(z0 - org_model_pred.to(device)).mean(dim=[1, 2, 3], keepdim=True)
541
+
542
+ grad = (reg_model_pred - org_model_pred.to(device)) / weighting_factor
543
+ loss = F.mse_loss(z0, (z0 - grad).detach())
544
+
545
+ return loss
546
+
547
+ def vsd_visualization(self, z0, noise, zt, reg_model_pred, org_model_pred, global_step, args):
548
+ #-------- Visualization --------#
549
+ # 1. Visualize latents, noise, zt
550
+ z0_img = self.model.decode(z0.to(dtype=torch.float32, device=self.model.vae.device))
551
+ ns_img = self.model.decode(noise.to(dtype=torch.float32, device=self.model.vae.device))
552
+ zt_img = self.model.decode(zt.to(dtype=torch.float32, device=self.model.vae.device))
553
+
554
+ z0_img_pil = transforms.ToPILImage()(torch.clamp(z0_img[0].cpu(), -1.0, 1.0) * 0.5 + 0.5)
555
+ ns_img_pil = transforms.ToPILImage()(torch.clamp(ns_img[0].cpu(), -1.0, 1.0) * 0.5 + 0.5)
556
+ zt_img_pil = transforms.ToPILImage()(torch.clamp(zt_img[0].cpu(), -1.0, 1.0) * 0.5 + 0.5)
557
+
558
+ # 2. Visualize reg_img, org_img
559
+ reg_img = self.model.decode(reg_model_pred.to(dtype=torch.float32, device=self.model.vae.device))
560
+ org_img = self.model.decode(org_model_pred.to(dtype=torch.float32, device=self.model.vae.device))
561
+
562
+ reg_img_pil = transforms.ToPILImage()(torch.clamp(reg_img[0].cpu(), -1.0, 1.0) * 0.5 + 0.5)
563
+ org_img_pil = transforms.ToPILImage()(torch.clamp(org_img[0].cpu(), -1.0, 1.0) * 0.5 + 0.5)
564
+
565
+ # Concatenate images side by side
566
+ w, h = z0_img_pil.width, z0_img_pil.height
567
+ combined_image = Image.new('RGB', (w*5, h))
568
+ combined_image.paste(z0_img_pil, (0, 0))
569
+ combined_image.paste(ns_img_pil, (w, 0))
570
+ combined_image.paste(zt_img_pil, (w*2, 0))
571
+ combined_image.paste(reg_img_pil, (w*3, 0))
572
+ combined_image.paste(org_img_pil, (w*4, 0))
573
+ combined_image.save(os.path.join(args.output_dir, f'visualization/vsd/{global_step}.png'))
574
+ #-------- Visualization --------#
575
+
576
+ def diff_loss(self, z0, prompt_embeds, pooled_embeds, net_lpips, args):
577
+
578
+ device = self.transformer_reg.device
579
+ u = compute_density_for_timestep_sampling(
580
+ weighting_scheme="uniform",
581
+ batch_size=1,
582
+ logit_mean=0.0,
583
+ logit_std=1.0,
584
+ mode_scale=1.29,
585
+ )
586
+
587
+ t_idx = (u*1000).long().to(device)
588
+ self.model.scheduler.set_timesteps(1000, device=device)
589
+ times = self.model.scheduler.timesteps
590
+ t = times[t_idx]
591
+ sigma = t / 1000
592
+
593
+ z0 = z0.to(device)
594
+ z0, prompt_embeds = z0.detach(), prompt_embeds.detach()
595
+ noise = torch.randn_like(z0)
596
+ sigma = sigma.half()
597
+ zt = (1-sigma) * z0 + sigma * noise # noisy latents
598
+
599
+ # v-prediction
600
+ v_pred = self.predict_vector_reg(zt, t, prompt_embeds.to(device), pooled_embeds.to(device))
601
+ model_pred = v_pred * (-sigma) + zt
602
+ target = z0
603
+
604
+ loss_weight = compute_loss_weighting_for_sd3("logit_normal", sigma)
605
+ diffusion_loss = loss_weight.float() * F.mse_loss(model_pred.float(), target.float())
606
+
607
+ loss_d = diffusion_loss
608
+
609
+ return loss_d.mean()
610
+
611
+ class OSEDiff_SD3_TEST(torch.nn.Module):
612
+ def __init__(self, args, base_model):
613
+ super().__init__()
614
+
615
+ self.args = args
616
+ self.model = base_model
617
+ self.lora_path = args.lora_path
618
+ self.vae_path = args.vae_path
619
+
620
+ # Add lora to transformer
621
+ print(f'Loading LoRA to Transformer from {self.lora_path}')
622
+ self.model.transformer.requires_grad_(False)
623
+ lora_params, _ = inject_lora(self.model.transformer, {"AdaLayerNormZero"}, loras=self.lora_path, r=args.lora_rank, verbose=False)
624
+ for name, param in self.model.transformer.named_parameters():
625
+ param.requires_grad = False
626
+
627
+ # Insert LoRA into VAE
628
+ print(f"Loading LoRA to VAE from {self.vae_path}")
629
+ self.model.vae, self.lora_vae_modules_encoder = inject_lora_vae(self.model.vae, lora_rank=args.lora_rank, verbose=False)
630
+ encoder_state_dict_fp16 = torch.load(self.vae_path, map_location="cpu", weights_only=True)
631
+ self.model.vae.encoder.load_state_dict(encoder_state_dict_fp16)
632
+
633
+ def predict_vector(self, z, t, prompt_emb, pooled_emb):
634
+ v = self.model.transformer(hidden_states=z,
635
+ timestep=t,
636
+ pooled_projections=pooled_emb,
637
+ encoder_hidden_states=prompt_emb,
638
+ return_dict=False)[0]
639
+ return v
640
+
641
+ @torch.no_grad()
642
+ def forward(self, x_src, prompt):
643
+
644
+ z_src = self.model.vae.encode(x_src.to(dtype=torch.float32, device=self.model.vae.device)).latent_dist.sample() * self.model.vae.config.scaling_factor
645
+
646
+ z_src = z_src.to(self.model.transformer.device)
647
+
648
+ # calculate prompt_embeddings and neg_prompt_embeddings
649
+ batch_size, _, _, _ = x_src.shape
650
+ with torch.no_grad():
651
+ prompt_embeds, pooled_embeds = self.model.encode_prompt([prompt], batch_size)
652
+
653
+ self.model.scheduler.set_timesteps(1, device=self.model.device)
654
+ timesteps = self.model.scheduler.timesteps
655
+
656
+ # Solve ODE
657
+ t = timesteps[0]
658
+ timestep = t.expand(z_src.shape[0]).to(self.model.transformer.device)
659
+ prompt_embeds = prompt_embeds.to(self.model.transformer.device, dtype=torch.float32)
660
+ pooled_embeds = pooled_embeds.to(self.model.transformer.device, dtype=torch.float32)
661
+ pred_v = self.predict_vector(z_src, timestep, prompt_embeds, pooled_embeds)
662
+
663
+ z_src = z_src - pred_v
664
+
665
+ with torch.no_grad():
666
+ output_image = self.model.decode(z_src.to(dtype=torch.float32, device=self.model.vae.device))
667
+
668
+ return output_image
669
+
670
+
671
+ class OSEDiff_SD3_TEST_efficient(torch.nn.Module):
672
+ def __init__(self, args, base_model):
673
+ super().__init__()
674
+
675
+ self.args = args
676
+ self.model = base_model
677
+ self.lora_path = args.lora_path
678
+ self.vae_path = args.vae_path
679
+
680
+ # Add lora to transformer
681
+ print(f'Loading LoRA to Transformer from {self.lora_path}')
682
+ self.model.transformer.requires_grad_(False)
683
+ lora_params, _ = inject_lora(self.model.transformer, {"AdaLayerNormZero"}, loras=self.lora_path, r=args.lora_rank, verbose=False)
684
+ for name, param in self.model.transformer.named_parameters():
685
+ param.requires_grad = False
686
+
687
+ # Insert LoRA into VAE
688
+ print(f"Loading LoRA to VAE from {self.vae_path}")
689
+ self.model.vae, self.lora_vae_modules_encoder = inject_lora_vae(self.model.vae, lora_rank=args.lora_rank, verbose=False)
690
+ encoder_state_dict_fp16 = torch.load(self.vae_path, map_location="cpu", weights_only=True)
691
+ self.model.vae.encoder.load_state_dict(encoder_state_dict_fp16)
692
+
693
+ def predict_vector(self, z, t, prompt_emb, pooled_emb):
694
+ v = self.model.transformer(hidden_states=z,
695
+ timestep=t,
696
+ pooled_projections=pooled_emb,
697
+ encoder_hidden_states=prompt_emb,
698
+ return_dict=False)[0]
699
+ return v
700
+
701
+ @torch.no_grad()
702
+ def forward(self, x_src, prompt):
703
+
704
+ z_src = self.model.vae.encode(x_src.to(dtype=torch.float32, device=self.model.vae.device)).latent_dist.sample() * self.model.vae.config.scaling_factor
705
+
706
+ z_src = z_src.to(self.model.transformer.device)
707
+
708
+ # calculate prompt_embeddings
709
+ batch_size, _, _, _ = x_src.shape
710
+ prompt_embeds, pooled_embeds = self.model.encode_prompt([prompt], batch_size)
711
+
712
+ self.model.scheduler.set_timesteps(1, device=self.model.device)
713
+ timesteps = self.model.scheduler.timesteps
714
+
715
+ # Solve ODE
716
+ t = timesteps[0]
717
+ timestep = t.expand(z_src.shape[0]).to(self.model.transformer.device)
718
+ prompt_embeds = prompt_embeds.to(self.model.transformer.device, dtype=torch.float32)
719
+ pooled_embeds = pooled_embeds.to(self.model.transformer.device, dtype=torch.float32)
720
+ pred_v = self.predict_vector(z_src, timestep, prompt_embeds, pooled_embeds)
721
+ z_src = z_src - pred_v
722
+
723
+ output_image = self.model.decode(z_src.to(dtype=torch.float32, device=self.model.vae.device))
724
+
725
+ return output_image
726
+
727
+
728
+ class OSEDiff_SD3_TEST_TILE(torch.nn.Module):
729
+ def __init__(self, args, base_model):
730
+ super().__init__()
731
+
732
+ self.args = args
733
+ self.model = base_model
734
+ self.lora_path = args.lora_path
735
+ self.vae_path = args.vae_path
736
+
737
+ # Add lora to transformer
738
+ print(f'Loading LoRA to Transformer from {self.lora_path}')
739
+ self.model.transformer.requires_grad_(False)
740
+ lora_params, _ = inject_lora(self.model.transformer, {"AdaLayerNormZero"}, loras=self.lora_path, r=args.lora_rank, verbose=False)
741
+ for name, param in self.model.transformer.named_parameters():
742
+ param.requires_grad = False
743
+
744
+ # Insert LoRA into VAE
745
+ print(f"Loading LoRA to VAE from {self.vae_path}")
746
+ self.model.vae, self.lora_vae_modules_encoder = inject_lora_vae(self.model.vae, lora_rank=args.lora_rank, verbose=False)
747
+ encoder_state_dict_fp16 = torch.load(self.vae_path, map_location="cpu", weights_only=True)
748
+ self.model.vae.encoder.load_state_dict(encoder_state_dict_fp16)
749
+
750
+ # save original forward (only once)
751
+ if not hasattr(self.model.vae.encoder, 'original_forward'):
752
+ setattr(self.model.vae.encoder, 'original_forward', self.model.vae.encoder.forward)
753
+ if not hasattr(self.model.vae.decoder, 'original_forward'):
754
+ setattr(self.model.vae.decoder, 'original_forward', self.model.vae.decoder.forward)
755
+ encoder_tile = args.vae_encoder_tiled_size
756
+ decoder_tile = args.vae_decoder_tiled_size
757
+ self.model.vae.encoder.forward = VAEHook(
758
+ self.model.vae.encoder,
759
+ tile_size=encoder_tile,
760
+ is_decoder=False,
761
+ fast_decoder=False,
762
+ fast_encoder=True,
763
+ color_fix=False,
764
+ to_gpu=True
765
+ )
766
+ self.model.vae.decoder.forward = VAEHook(
767
+ self.model.vae.decoder,
768
+ tile_size=decoder_tile,
769
+ is_decoder=True,
770
+ fast_decoder=True,
771
+ fast_encoder=False,
772
+ color_fix=False,
773
+ to_gpu=True
774
+ )
775
+
776
+ def predict_vector(self, z, t, prompt_emb, pooled_emb):
777
+ v = self.model.transformer(hidden_states=z,
778
+ timestep=t,
779
+ pooled_projections=pooled_emb,
780
+ encoder_hidden_states=prompt_emb,
781
+ return_dict=False)[0]
782
+ return v
783
+
784
+ @torch.no_grad()
785
+ def create_full_latent(self, x_full: torch.Tensor, vlm_model, vlm_processor, full_path, next_path, prompt_type) -> torch.Tensor:
786
+ device = self.model.transformer.device
787
+ # 1) encode to full latent (via VAEHook)
788
+ z_full = self.model.vae.encode(x_full).latent_dist.sample() \
789
+ * self.model.vae.config.scaling_factor
790
+ z_full = z_full.to(device)
791
+ B, C, H, W = z_full.shape
792
+
793
+ # 2) grid size
794
+ tsize = self.args.latent_tiled_size
795
+ tover = self.args.latent_tiled_overlap
796
+ stride = tsize - tover
797
+ rows = (H - tsize + stride - 1)//stride + 1
798
+ cols = (W - tsize + stride - 1)//stride + 1
799
+ print(f'TILE SIZE: {tsize}, TILE OVERLAP: {tover}, STRIDE: {stride}, ROWS: {rows}, COLS: {cols}')
800
+
801
+ # 3) make gaussian weight patched [B,C,tsize,tsize]
802
+ weights = self._make_gaussian(tsize, tsize, 1).to(z_full.device)
803
+
804
+ # 4) collect all patches
805
+ positions = []
806
+ out_tiles = []
807
+ timestep = self.model.scheduler.timesteps[0].expand(B).to(z_full.device)
808
+
809
+ for i in range(rows):
810
+ y0 = min(i*stride, H - tsize)
811
+ for j in range(cols):
812
+ x0 = min(j*stride, W - tsize)
813
+ positions.append((y0, x0))
814
+ patch = z_full[:, :, y0:y0+tsize, x0:x0+tsize]
815
+
816
+ # decode and save patch for later usage
817
+ patch_path = f'{next_path[:-4]}_patch_row{i}col{j}.png'
818
+ patch_img = self.decode_full_latent(patch)
819
+ patch_pil = transforms.ToPILImage()((patch_img[0] * 0.5 + 0.5).clamp(0,1))
820
+ patch_pil.save(patch_path)
821
+
822
+ # create prompt to explain patch (CoZ)
823
+ prompt = self.create_prompt(vlm_model, vlm_processor, full_path, patch_path, prompt_type)
824
+ print('PROMPT: ', prompt)
825
+ prompt_emb, pooled_emb = self.model.encode_prompt([prompt], batch_size=B)
826
+ prompt_emb = prompt_emb.to(z_full.device, dtype=torch.float32)
827
+ pooled_emb = pooled_emb.to(z_full.device, dtype=torch.float32)
828
+
829
+ v = self.predict_vector(patch, timestep, prompt_emb, pooled_emb)
830
+ out_tiles.append(patch - v)
831
+
832
+ # 5) accumulate + normalize
833
+ z_out = torch.zeros_like(z_full)
834
+ z_norm = torch.zeros_like(z_full)
835
+ norm = torch.zeros_like(z_full)
836
+
837
+ for (y0,x0), tile in zip(positions, out_tiles):
838
+ z_out[:, :, y0:y0+tsize, x0:x0+tsize] += tile
839
+ z_norm[:, :, y0:y0+tsize, x0:x0+tsize] += tile * weights
840
+ norm[:, :, y0:y0+tsize, x0:x0+tsize] += weights
841
+
842
+ # 6) avoid division by zero and finalize
843
+ eps = 1e-10
844
+ z_norm = z_norm / (norm + eps)
845
+ return z_norm, z_out
846
+
847
+ @torch.no_grad()
848
+ def decode_full_latent(self, z_full: torch.Tensor) -> torch.Tensor:
849
+ """
850
+ Decode the tiled full latent into an RGB image (with tiled VAE decoder).
851
+ """
852
+ z_full = z_full.to(self.model.vae.device)
853
+ img = self.model.vae.decode(z_full / self.model.vae.config.scaling_factor).sample
854
+ return img.clamp(-1,1)
855
+
856
+ @torch.no_grad()
857
+ def create_prompt(self, vlm_model, vlm_processor, full_path, patch_path, prompt_type):
858
+ if prompt_type in ('vlm','vlm_base'):
859
+ from qwen_vl_utils import process_vision_info
860
+
861
+ message_text = None
862
+ start_image_path = full_path
863
+ input_image_path = patch_path
864
+
865
+ message_text = "The second image is a zoom-in of the first image. Based on this knowledge, what is in the second image? Give me a set of words."
866
+ messages = [
867
+ {"role": "system", "content": f"{message_text}"},
868
+ {
869
+ "role": "user",
870
+ "content": [
871
+ {"type": "image", "image": start_image_path},
872
+ {"type": "image", "image": input_image_path}
873
+ ]
874
+ }
875
+ ]
876
+
877
+ text = vlm_processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
878
+ image_inputs, video_inputs = process_vision_info(messages)
879
+ inputs = vlm_processor(
880
+ text=[text],
881
+ images=image_inputs,
882
+ videos=video_inputs,
883
+ padding=True,
884
+ return_tensors="pt",
885
+ )
886
+ generated_ids = vlm_model.generate(**inputs, max_new_tokens=16)
887
+ generated_ids_trimmed = [
888
+ out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
889
+ ]
890
+ output_text = vlm_processor.batch_decode(
891
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
892
+ )
893
+
894
+ prompt_text = output_text[0]
895
+ return prompt_text
896
+ else:
897
+ raise ValueError(f"Unknown prompt_type: {prompt_type}")
898
+
899
+ def _make_gaussian(self, w, h, nb):
900
+ from numpy import pi, exp, sqrt
901
+ import numpy as np
902
+
903
+ latent_width = w
904
+ latent_height = h
905
+
906
+ var = 0.01
907
+ midpoint = (latent_width - 1) / 2 # -1 because index goes from 0 to latent_width - 1
908
+ x_probs = [exp(-(x-midpoint)*(x-midpoint)/(latent_width*latent_width)/(2*var)) / sqrt(2*pi*var) for x in range(latent_width)]
909
+ midpoint = latent_height / 2
910
+ y_probs = [exp(-(y-midpoint)*(y-midpoint)/(latent_height*latent_height)/(2*var)) / sqrt(2*pi*var) for y in range(latent_height)]
911
+
912
+ weights = np.outer(y_probs, x_probs)
913
+ return torch.tile(torch.tensor(weights, device=self.model.vae.device), (nb, self.model.vae.config.latent_channels, 1, 1))
vendor/utils/devices.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import contextlib
3
+ from functools import lru_cache
4
+
5
+ import torch
6
+ #from modules import errors
7
+
8
+ if sys.platform == "darwin":
9
+ from modules import mac_specific
10
+
11
+
12
+ def has_mps() -> bool:
13
+ if sys.platform != "darwin":
14
+ return False
15
+ else:
16
+ return mac_specific.has_mps
17
+
18
+
19
+ def get_cuda_device_string():
20
+ return "cuda"
21
+
22
+
23
+ def get_optimal_device_name():
24
+ if torch.cuda.is_available():
25
+ return get_cuda_device_string()
26
+
27
+ if has_mps():
28
+ return "mps"
29
+
30
+ return "cpu"
31
+
32
+
33
+ def get_optimal_device():
34
+ return torch.device(get_optimal_device_name())
35
+
36
+
37
+ def get_device_for(task):
38
+ return get_optimal_device()
39
+
40
+
41
+ def torch_gc():
42
+
43
+ if torch.cuda.is_available():
44
+ with torch.cuda.device(get_cuda_device_string()):
45
+ torch.cuda.empty_cache()
46
+ torch.cuda.ipc_collect()
47
+
48
+ if has_mps():
49
+ mac_specific.torch_mps_gc()
50
+
51
+
52
+ def enable_tf32():
53
+ if torch.cuda.is_available():
54
+
55
+ # enabling benchmark option seems to enable a range of cards to do fp16 when they otherwise can't
56
+ # see https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/4407
57
+ if any(torch.cuda.get_device_capability(devid) == (7, 5) for devid in range(0, torch.cuda.device_count())):
58
+ torch.backends.cudnn.benchmark = True
59
+
60
+ torch.backends.cuda.matmul.allow_tf32 = True
61
+ torch.backends.cudnn.allow_tf32 = True
62
+
63
+
64
+ enable_tf32()
65
+ #errors.run(enable_tf32, "Enabling TF32")
66
+
67
+ cpu = torch.device("cpu")
68
+ device = device_interrogate = device_gfpgan = device_esrgan = device_codeformer = torch.device("cuda")
69
+ dtype = torch.float16
70
+ dtype_vae = torch.float16
71
+ dtype_unet = torch.float16
72
+ unet_needs_upcast = False
73
+
74
+
75
+ def cond_cast_unet(input):
76
+ return input.to(dtype_unet) if unet_needs_upcast else input
77
+
78
+
79
+ def cond_cast_float(input):
80
+ return input.float() if unet_needs_upcast else input
81
+
82
+
83
+ def randn(seed, shape):
84
+ torch.manual_seed(seed)
85
+ return torch.randn(shape, device=device)
86
+
87
+
88
+ def randn_without_seed(shape):
89
+ return torch.randn(shape, device=device)
90
+
91
+
92
+ def autocast(disable=False):
93
+ if disable:
94
+ return contextlib.nullcontext()
95
+
96
+ return torch.autocast("cuda")
97
+
98
+
99
+ def without_autocast(disable=False):
100
+ return torch.autocast("cuda", enabled=False) if torch.is_autocast_enabled() and not disable else contextlib.nullcontext()
101
+
102
+
103
+ class NansException(Exception):
104
+ pass
105
+
106
+
107
+ def test_for_nans(x, where):
108
+ if not torch.all(torch.isnan(x)).item():
109
+ return
110
+
111
+ if where == "unet":
112
+ message = "A tensor with all NaNs was produced in Unet."
113
+
114
+ elif where == "vae":
115
+ message = "A tensor with all NaNs was produced in VAE."
116
+
117
+ else:
118
+ message = "A tensor with all NaNs was produced."
119
+
120
+ message += " Use --disable-nan-check commandline argument to disable this check."
121
+
122
+ raise NansException(message)
123
+
124
+
125
+ @lru_cache
126
+ def first_time_calculation():
127
+ """
128
+ just do any calculation with pytorch layers - the first time this is done it allocaltes about 700MB of memory and
129
+ spends about 2.7 seconds doing that, at least wih NVidia.
130
+ """
131
+
132
+ x = torch.zeros((1, 1)).to(device, dtype)
133
+ linear = torch.nn.Linear(1, 1).to(device, dtype)
134
+ linear(x)
135
+
136
+ x = torch.zeros((1, 1, 3, 3)).to(device, dtype)
137
+ conv2d = torch.nn.Conv2d(1, 1, (3, 3)).to(device, dtype)
138
+ conv2d(x)
vendor/utils/vaehook.py ADDED
@@ -0,0 +1,829 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ------------------------------------------------------------------------
2
+ #
3
+ # Ultimate VAE Tile Optimization
4
+ #
5
+ # Introducing a revolutionary new optimization designed to make
6
+ # the VAE work with giant images on limited VRAM!
7
+ # Say goodbye to the frustration of OOM and hello to seamless output!
8
+ #
9
+ # ------------------------------------------------------------------------
10
+ #
11
+ # This script is a wild hack that splits the image into tiles,
12
+ # encodes each tile separately, and merges the result back together.
13
+ #
14
+ # Advantages:
15
+ # - The VAE can now work with giant images on limited VRAM
16
+ # (~10 GB for 8K images!)
17
+ # - The merged output is completely seamless without any post-processing.
18
+ #
19
+ # Drawbacks:
20
+ # - Giant RAM needed. To store the intermediate results for a 4096x4096
21
+ # images, you need 32 GB RAM it consumes ~20GB); for 8192x8192
22
+ # you need 128 GB RAM machine (it consumes ~100 GB)
23
+ # - NaNs always appear in for 8k images when you use fp16 (half) VAE
24
+ # You must use --no-half-vae to disable half VAE for that giant image.
25
+ # - Slow speed. With default tile size, it takes around 50/200 seconds
26
+ # to encode/decode a 4096x4096 image; and 200/900 seconds to encode/decode
27
+ # a 8192x8192 image. (The speed is limited by both the GPU and the CPU.)
28
+ # - The gradient calculation is not compatible with this hack. It
29
+ # will break any backward() or torch.autograd.grad() that passes VAE.
30
+ # (But you can still use the VAE to generate training data.)
31
+ #
32
+ # How it works:
33
+ # 1) The image is split into tiles.
34
+ # - To ensure perfect results, each tile is padded with 32 pixels
35
+ # on each side.
36
+ # - Then the conv2d/silu/upsample/downsample can produce identical
37
+ # results to the original image without splitting.
38
+ # 2) The original forward is decomposed into a task queue and a task worker.
39
+ # - The task queue is a list of functions that will be executed in order.
40
+ # - The task worker is a loop that executes the tasks in the queue.
41
+ # 3) The task queue is executed for each tile.
42
+ # - Current tile is sent to GPU.
43
+ # - local operations are directly executed.
44
+ # - Group norm calculation is temporarily suspended until the mean
45
+ # and var of all tiles are calculated.
46
+ # - The residual is pre-calculated and stored and addded back later.
47
+ # - When need to go to the next tile, the current tile is send to cpu.
48
+ # 4) After all tiles are processed, tiles are merged on cpu and return.
49
+ #
50
+ # Enjoy!
51
+ #
52
+ # @author: LI YI @ Nanyang Technological University - Singapore
53
+ # @date: 2023-03-02
54
+ # @license: MIT License
55
+ #
56
+ # Please give me a star if you like this project!
57
+ #
58
+ # -------------------------------------------------------------------------
59
+
60
+ import gc
61
+ from time import time
62
+ import math
63
+ from tqdm import tqdm
64
+
65
+ import torch
66
+ import torch.version
67
+ import torch.nn.functional as F
68
+ from einops import rearrange
69
+ import os
70
+ import sys
71
+ sys.path.append(os.getcwd())
72
+ import utils.devices as devices
73
+
74
+ try:
75
+ import xformers
76
+ import xformers.ops
77
+ except ImportError:
78
+ pass
79
+
80
+ sd_flag = False
81
+
82
+ def get_recommend_encoder_tile_size():
83
+ if torch.cuda.is_available():
84
+ total_memory = torch.cuda.get_device_properties(
85
+ devices.device).total_memory // 2**20
86
+ if total_memory > 16*1000:
87
+ ENCODER_TILE_SIZE = 3072
88
+ elif total_memory > 12*1000:
89
+ ENCODER_TILE_SIZE = 2048
90
+ elif total_memory > 8*1000:
91
+ ENCODER_TILE_SIZE = 1536
92
+ else:
93
+ ENCODER_TILE_SIZE = 960
94
+ else:
95
+ ENCODER_TILE_SIZE = 512
96
+ return ENCODER_TILE_SIZE
97
+
98
+
99
+ def get_recommend_decoder_tile_size():
100
+ if torch.cuda.is_available():
101
+ total_memory = torch.cuda.get_device_properties(
102
+ devices.device).total_memory // 2**20
103
+ if total_memory > 30*1000:
104
+ DECODER_TILE_SIZE = 256
105
+ elif total_memory > 16*1000:
106
+ DECODER_TILE_SIZE = 192
107
+ elif total_memory > 12*1000:
108
+ DECODER_TILE_SIZE = 128
109
+ elif total_memory > 8*1000:
110
+ DECODER_TILE_SIZE = 96
111
+ else:
112
+ DECODER_TILE_SIZE = 64
113
+ else:
114
+ DECODER_TILE_SIZE = 64
115
+ return DECODER_TILE_SIZE
116
+
117
+
118
+ if 'global const':
119
+ DEFAULT_ENABLED = False
120
+ DEFAULT_MOVE_TO_GPU = False
121
+ DEFAULT_FAST_ENCODER = True
122
+ DEFAULT_FAST_DECODER = True
123
+ DEFAULT_COLOR_FIX = 0
124
+ DEFAULT_ENCODER_TILE_SIZE = get_recommend_encoder_tile_size()
125
+ DEFAULT_DECODER_TILE_SIZE = get_recommend_decoder_tile_size()
126
+
127
+
128
+ # inplace version of silu
129
+ def inplace_nonlinearity(x):
130
+ # Test: fix for Nans
131
+ return F.silu(x, inplace=True)
132
+
133
+ # extracted from ldm.modules.diffusionmodules.model
134
+
135
+ # from diffusers lib
136
+ def attn_forward_new(self, h_):
137
+ batch_size, channel, height, width = h_.shape
138
+ hidden_states = h_.view(batch_size, channel, height * width).transpose(1, 2)
139
+
140
+ attention_mask = None
141
+ encoder_hidden_states = None
142
+ batch_size, sequence_length, _ = hidden_states.shape
143
+ attention_mask = self.prepare_attention_mask(attention_mask, sequence_length, batch_size)
144
+
145
+ query = self.to_q(hidden_states)
146
+
147
+ if encoder_hidden_states is None:
148
+ encoder_hidden_states = hidden_states
149
+ elif self.norm_cross:
150
+ encoder_hidden_states = self.norm_encoder_hidden_states(encoder_hidden_states)
151
+
152
+ key = self.to_k(encoder_hidden_states)
153
+ value = self.to_v(encoder_hidden_states)
154
+
155
+ query = self.head_to_batch_dim(query)
156
+ key = self.head_to_batch_dim(key)
157
+ value = self.head_to_batch_dim(value)
158
+
159
+ attention_probs = self.get_attention_scores(query, key, attention_mask)
160
+ hidden_states = torch.bmm(attention_probs, value)
161
+ hidden_states = self.batch_to_head_dim(hidden_states)
162
+
163
+ # linear proj
164
+ hidden_states = self.to_out[0](hidden_states)
165
+ # dropout
166
+ hidden_states = self.to_out[1](hidden_states)
167
+
168
+ hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width)
169
+
170
+ return hidden_states
171
+
172
+ def attn_forward(self, h_):
173
+ q = self.q(h_)
174
+ k = self.k(h_)
175
+ v = self.v(h_)
176
+
177
+ # compute attention
178
+ b, c, h, w = q.shape
179
+ q = q.reshape(b, c, h*w)
180
+ q = q.permute(0, 2, 1) # b,hw,c
181
+ k = k.reshape(b, c, h*w) # b,c,hw
182
+ w_ = torch.bmm(q, k) # b,hw,hw w[b,i,j]=sum_c q[b,i,c]k[b,c,j]
183
+ w_ = w_ * (int(c)**(-0.5))
184
+ w_ = torch.nn.functional.softmax(w_, dim=2)
185
+
186
+ # attend to values
187
+ v = v.reshape(b, c, h*w)
188
+ w_ = w_.permute(0, 2, 1) # b,hw,hw (first hw of k, second of q)
189
+ # b, c,hw (hw of q) h_[b,c,j] = sum_i v[b,c,i] w_[b,i,j]
190
+ h_ = torch.bmm(v, w_)
191
+ h_ = h_.reshape(b, c, h, w)
192
+
193
+ h_ = self.proj_out(h_)
194
+
195
+ return h_
196
+
197
+
198
+ def xformer_attn_forward(self, h_):
199
+ q = self.q(h_)
200
+ k = self.k(h_)
201
+ v = self.v(h_)
202
+
203
+ # compute attention
204
+ B, C, H, W = q.shape
205
+ q, k, v = map(lambda x: rearrange(x, 'b c h w -> b (h w) c'), (q, k, v))
206
+
207
+ q, k, v = map(
208
+ lambda t: t.unsqueeze(3)
209
+ .reshape(B, t.shape[1], 1, C)
210
+ .permute(0, 2, 1, 3)
211
+ .reshape(B * 1, t.shape[1], C)
212
+ .contiguous(),
213
+ (q, k, v),
214
+ )
215
+ out = xformers.ops.memory_efficient_attention(
216
+ q, k, v, attn_bias=None, op=self.attention_op)
217
+
218
+ out = (
219
+ out.unsqueeze(0)
220
+ .reshape(B, 1, out.shape[1], C)
221
+ .permute(0, 2, 1, 3)
222
+ .reshape(B, out.shape[1], C)
223
+ )
224
+ out = rearrange(out, 'b (h w) c -> b c h w', b=B, h=H, w=W, c=C)
225
+ out = self.proj_out(out)
226
+ return out
227
+
228
+
229
+ def attn2task(task_queue, net):
230
+ if False: #isinstance(net, AttnBlock):
231
+ task_queue.append(('store_res', lambda x: x))
232
+ task_queue.append(('pre_norm', net.norm))
233
+ task_queue.append(('attn', lambda x, net=net: attn_forward(net, x)))
234
+ task_queue.append(['add_res', None])
235
+ elif False: #isinstance(net, MemoryEfficientAttnBlock):
236
+ task_queue.append(('store_res', lambda x: x))
237
+ task_queue.append(('pre_norm', net.norm))
238
+ task_queue.append(
239
+ ('attn', lambda x, net=net: xformer_attn_forward(net, x)))
240
+ task_queue.append(['add_res', None])
241
+ else:
242
+ task_queue.append(('store_res', lambda x: x))
243
+ task_queue.append(('pre_norm', net.group_norm))
244
+ task_queue.append(('attn', lambda x, net=net: attn_forward_new(net, x)))
245
+ task_queue.append(['add_res', None])
246
+
247
+ def resblock2task(queue, block):
248
+ """
249
+ Turn a ResNetBlock into a sequence of tasks and append to the task queue
250
+
251
+ @param queue: the target task queue
252
+ @param block: ResNetBlock
253
+
254
+ """
255
+ if block.in_channels != block.out_channels:
256
+ if sd_flag:
257
+ if block.use_conv_shortcut:
258
+ queue.append(('store_res', block.conv_shortcut))
259
+ else:
260
+ queue.append(('store_res', block.nin_shortcut))
261
+ else:
262
+ if block.use_in_shortcut:
263
+ queue.append(('store_res', block.conv_shortcut))
264
+ else:
265
+ queue.append(('store_res', block.nin_shortcut))
266
+
267
+ else:
268
+ queue.append(('store_res', lambda x: x))
269
+ queue.append(('pre_norm', block.norm1))
270
+ queue.append(('silu', inplace_nonlinearity))
271
+ queue.append(('conv1', block.conv1))
272
+ queue.append(('pre_norm', block.norm2))
273
+ queue.append(('silu', inplace_nonlinearity))
274
+ queue.append(('conv2', block.conv2))
275
+ queue.append(['add_res', None])
276
+
277
+
278
+
279
+ def build_sampling(task_queue, net, is_decoder):
280
+ """
281
+ Build the sampling part of a task queue
282
+ @param task_queue: the target task queue
283
+ @param net: the network
284
+ @param is_decoder: currently building decoder or encoder
285
+ """
286
+ if is_decoder:
287
+ # resblock2task(task_queue, net.mid.block_1)
288
+ # attn2task(task_queue, net.mid.attn_1)
289
+ # resblock2task(task_queue, net.mid.block_2)
290
+ # resolution_iter = reversed(range(net.num_resolutions))
291
+ # block_ids = net.num_res_blocks + 1
292
+ # condition = 0
293
+ # module = net.up
294
+ # func_name = 'upsample'
295
+ resblock2task(task_queue, net.mid_block.resnets[0])
296
+ attn2task(task_queue, net.mid_block.attentions[0])
297
+ resblock2task(task_queue, net.mid_block.resnets[1])
298
+ resolution_iter = (range(len(net.up_blocks))) # range(0,4)
299
+ block_ids = 2 + 1
300
+ condition = len(net.up_blocks) - 1
301
+ module = net.up_blocks
302
+ func_name = 'upsamplers'
303
+ else:
304
+ # resolution_iter = range(net.num_resolutions)
305
+ # block_ids = net.num_res_blocks
306
+ # condition = net.num_resolutions - 1
307
+ # module = net.down
308
+ # func_name = 'downsample'
309
+ resolution_iter = (range(len(net.down_blocks))) # range(0,4)
310
+ block_ids = 2
311
+ condition = len(net.down_blocks) - 1
312
+ module = net.down_blocks
313
+ func_name = 'downsamplers'
314
+
315
+
316
+ for i_level in resolution_iter:
317
+ for i_block in range(block_ids):
318
+ resblock2task(task_queue, module[i_level].resnets[i_block])
319
+ if i_level != condition:
320
+ if is_decoder:
321
+ task_queue.append((func_name, module[i_level].upsamplers[0]))
322
+ else:
323
+ task_queue.append((func_name, module[i_level].downsamplers[0]))
324
+
325
+ if not is_decoder:
326
+ resblock2task(task_queue, net.mid_block.resnets[0])
327
+ attn2task(task_queue, net.mid_block.attentions[0])
328
+ resblock2task(task_queue, net.mid_block.resnets[1])
329
+
330
+
331
+ def build_task_queue(net, is_decoder):
332
+ """
333
+ Build a single task queue for the encoder or decoder
334
+ @param net: the VAE decoder or encoder network
335
+ @param is_decoder: currently building decoder or encoder
336
+ @return: the task queue
337
+ """
338
+ task_queue = []
339
+ task_queue.append(('conv_in', net.conv_in))
340
+
341
+ # construct the sampling part of the task queue
342
+ # because encoder and decoder share the same architecture, we extract the sampling part
343
+ build_sampling(task_queue, net, is_decoder)
344
+ if is_decoder and not sd_flag:
345
+ net.give_pre_end = False
346
+ net.tanh_out = False
347
+
348
+ if not is_decoder or not net.give_pre_end:
349
+ if sd_flag:
350
+ task_queue.append(('pre_norm', net.norm_out))
351
+ else:
352
+ task_queue.append(('pre_norm', net.conv_norm_out))
353
+ task_queue.append(('silu', inplace_nonlinearity))
354
+ task_queue.append(('conv_out', net.conv_out))
355
+ if is_decoder and net.tanh_out:
356
+ task_queue.append(('tanh', torch.tanh))
357
+
358
+ return task_queue
359
+
360
+
361
+ def clone_task_queue(task_queue):
362
+ """
363
+ Clone a task queue
364
+ @param task_queue: the task queue to be cloned
365
+ @return: the cloned task queue
366
+ """
367
+ return [[item for item in task] for task in task_queue]
368
+
369
+
370
+ def get_var_mean(input, num_groups, eps=1e-6):
371
+ """
372
+ Get mean and var for group norm
373
+ """
374
+ b, c = input.size(0), input.size(1)
375
+ channel_in_group = int(c/num_groups)
376
+ input_reshaped = input.contiguous().view(
377
+ 1, int(b * num_groups), channel_in_group, *input.size()[2:])
378
+ var, mean = torch.var_mean(
379
+ input_reshaped, dim=[0, 2, 3, 4], unbiased=False)
380
+ return var, mean
381
+
382
+
383
+ def custom_group_norm(input, num_groups, mean, var, weight=None, bias=None, eps=1e-6):
384
+ """
385
+ Custom group norm with fixed mean and var
386
+
387
+ @param input: input tensor
388
+ @param num_groups: number of groups. by default, num_groups = 32
389
+ @param mean: mean, must be pre-calculated by get_var_mean
390
+ @param var: var, must be pre-calculated by get_var_mean
391
+ @param weight: weight, should be fetched from the original group norm
392
+ @param bias: bias, should be fetched from the original group norm
393
+ @param eps: epsilon, by default, eps = 1e-6 to match the original group norm
394
+
395
+ @return: normalized tensor
396
+ """
397
+ b, c = input.size(0), input.size(1)
398
+ channel_in_group = int(c/num_groups)
399
+ input_reshaped = input.contiguous().view(
400
+ 1, int(b * num_groups), channel_in_group, *input.size()[2:])
401
+
402
+ out = F.batch_norm(input_reshaped, mean, var, weight=None, bias=None,
403
+ training=False, momentum=0, eps=eps)
404
+
405
+ out = out.view(b, c, *input.size()[2:])
406
+
407
+ # post affine transform
408
+ if weight is not None:
409
+ out *= weight.view(1, -1, 1, 1)
410
+ if bias is not None:
411
+ out += bias.view(1, -1, 1, 1)
412
+ return out
413
+
414
+
415
+ def crop_valid_region(x, input_bbox, target_bbox, is_decoder):
416
+ """
417
+ Crop the valid region from the tile
418
+ @param x: input tile
419
+ @param input_bbox: original input bounding box
420
+ @param target_bbox: output bounding box
421
+ @param scale: scale factor
422
+ @return: cropped tile
423
+ """
424
+ padded_bbox = [i * 8 if is_decoder else i//8 for i in input_bbox]
425
+ margin = [target_bbox[i] - padded_bbox[i] for i in range(4)]
426
+ return x[:, :, margin[2]:x.size(2)+margin[3], margin[0]:x.size(3)+margin[1]]
427
+
428
+ # ↓↓↓ https://github.com/Kahsolt/stable-diffusion-webui-vae-tile-infer ↓↓↓
429
+
430
+
431
+ def perfcount(fn):
432
+ def wrapper(*args, **kwargs):
433
+ ts = time()
434
+
435
+ if torch.cuda.is_available():
436
+ torch.cuda.reset_peak_memory_stats(devices.device)
437
+ devices.torch_gc()
438
+ gc.collect()
439
+
440
+ ret = fn(*args, **kwargs)
441
+
442
+ devices.torch_gc()
443
+ gc.collect()
444
+ if torch.cuda.is_available():
445
+ vram = torch.cuda.max_memory_allocated(devices.device) / 2**20
446
+ torch.cuda.reset_peak_memory_stats(devices.device)
447
+ print(
448
+ f'[Tiled VAE]: Done in {time() - ts:.3f}s, max VRAM alloc {vram:.3f} MB')
449
+ else:
450
+ print(f'[Tiled VAE]: Done in {time() - ts:.3f}s')
451
+
452
+ return ret
453
+ return wrapper
454
+
455
+ # copy end :)
456
+
457
+
458
+ class GroupNormParam:
459
+ def __init__(self):
460
+ self.var_list = []
461
+ self.mean_list = []
462
+ self.pixel_list = []
463
+ self.weight = None
464
+ self.bias = None
465
+
466
+ def add_tile(self, tile, layer):
467
+ var, mean = get_var_mean(tile, 32)
468
+ # For giant images, the variance can be larger than max float16
469
+ # In this case we create a copy to float32
470
+ if var.dtype == torch.float16 and var.isinf().any():
471
+ fp32_tile = tile.float()
472
+ var, mean = get_var_mean(fp32_tile, 32)
473
+ # ============= DEBUG: test for infinite =============
474
+ # if torch.isinf(var).any():
475
+ # print('var: ', var)
476
+ # ====================================================
477
+ self.var_list.append(var)
478
+ self.mean_list.append(mean)
479
+ self.pixel_list.append(
480
+ tile.shape[2]*tile.shape[3])
481
+ if hasattr(layer, 'weight'):
482
+ self.weight = layer.weight
483
+ self.bias = layer.bias
484
+ else:
485
+ self.weight = None
486
+ self.bias = None
487
+
488
+ def summary(self):
489
+ """
490
+ summarize the mean and var and return a function
491
+ that apply group norm on each tile
492
+ """
493
+ if len(self.var_list) == 0:
494
+ return None
495
+ var = torch.vstack(self.var_list)
496
+ mean = torch.vstack(self.mean_list)
497
+ max_value = max(self.pixel_list)
498
+ pixels = torch.tensor(
499
+ self.pixel_list, dtype=torch.float32, device=devices.device) / max_value
500
+ sum_pixels = torch.sum(pixels)
501
+ pixels = pixels.unsqueeze(
502
+ 1) / sum_pixels
503
+ var = torch.sum(
504
+ var * pixels, dim=0)
505
+ mean = torch.sum(
506
+ mean * pixels, dim=0)
507
+ return lambda x: custom_group_norm(x, 32, mean, var, self.weight, self.bias)
508
+
509
+ @staticmethod
510
+ def from_tile(tile, norm):
511
+ """
512
+ create a function from a single tile without summary
513
+ """
514
+ var, mean = get_var_mean(tile, 32)
515
+ if var.dtype == torch.float16 and var.isinf().any():
516
+ fp32_tile = tile.float()
517
+ var, mean = get_var_mean(fp32_tile, 32)
518
+ # if it is a macbook, we need to convert back to float16
519
+ if var.device.type == 'mps':
520
+ # clamp to avoid overflow
521
+ var = torch.clamp(var, 0, 60000)
522
+ var = var.half()
523
+ mean = mean.half()
524
+ if hasattr(norm, 'weight'):
525
+ weight = norm.weight
526
+ bias = norm.bias
527
+ else:
528
+ weight = None
529
+ bias = None
530
+
531
+ def group_norm_func(x, mean=mean, var=var, weight=weight, bias=bias):
532
+ return custom_group_norm(x, 32, mean, var, weight, bias, 1e-6)
533
+ return group_norm_func
534
+
535
+
536
+ class VAEHook:
537
+ def __init__(self, net, tile_size, is_decoder, fast_decoder, fast_encoder, color_fix, to_gpu=False):
538
+ self.net = net # encoder | decoder
539
+ self.tile_size = tile_size
540
+ self.is_decoder = is_decoder
541
+ self.fast_mode = (fast_encoder and not is_decoder) or (
542
+ fast_decoder and is_decoder)
543
+ self.color_fix = color_fix and not is_decoder
544
+ self.to_gpu = to_gpu
545
+ self.pad = 11 if is_decoder else 32
546
+
547
+ def __call__(self, x):
548
+ B, C, H, W = x.shape
549
+ original_device = next(self.net.parameters()).device
550
+ try:
551
+ if self.to_gpu:
552
+ # self.net.to(devices.get_optimal_device())
553
+ self.net.to(original_device)
554
+ if max(H, W) <= self.pad * 2 + self.tile_size:
555
+ # print("[Tiled VAE]: the input size is tiny and unnecessary to tile.")
556
+ return self.net.original_forward(x).to(original_device)
557
+ else:
558
+ return self.vae_tile_forward(x)
559
+ finally:
560
+ self.net.to(original_device)
561
+
562
+ def get_best_tile_size(self, lowerbound, upperbound):
563
+ """
564
+ Get the best tile size for GPU memory
565
+ """
566
+ divider = 32
567
+ while divider >= 2:
568
+ remainer = lowerbound % divider
569
+ if remainer == 0:
570
+ return lowerbound
571
+ candidate = lowerbound - remainer + divider
572
+ if candidate <= upperbound:
573
+ return candidate
574
+ divider //= 2
575
+ return lowerbound
576
+
577
+ def split_tiles(self, h, w):
578
+ """
579
+ Tool function to split the image into tiles
580
+ @param h: height of the image
581
+ @param w: width of the image
582
+ @return: tile_input_bboxes, tile_output_bboxes
583
+ """
584
+ tile_input_bboxes, tile_output_bboxes = [], []
585
+ tile_size = self.tile_size
586
+ pad = self.pad
587
+ num_height_tiles = math.ceil((h - 2 * pad) / tile_size)
588
+ num_width_tiles = math.ceil((w - 2 * pad) / tile_size)
589
+ # If any of the numbers are 0, we let it be 1
590
+ # This is to deal with long and thin images
591
+ num_height_tiles = max(num_height_tiles, 1)
592
+ num_width_tiles = max(num_width_tiles, 1)
593
+
594
+ # Suggestions from https://github.com/Kahsolt: auto shrink the tile size
595
+ real_tile_height = math.ceil((h - 2 * pad) / num_height_tiles)
596
+ real_tile_width = math.ceil((w - 2 * pad) / num_width_tiles)
597
+ real_tile_height = self.get_best_tile_size(real_tile_height, tile_size)
598
+ real_tile_width = self.get_best_tile_size(real_tile_width, tile_size)
599
+
600
+ print(f'[Tiled VAE]: split to {num_height_tiles}x{num_width_tiles} = {num_height_tiles*num_width_tiles} tiles. ' +
601
+ f'Optimal tile size {real_tile_width}x{real_tile_height}, original tile size {tile_size}x{tile_size}')
602
+
603
+ for i in range(num_height_tiles):
604
+ for j in range(num_width_tiles):
605
+ # bbox: [x1, x2, y1, y2]
606
+ # the padding is is unnessary for image borders. So we directly start from (32, 32)
607
+ input_bbox = [
608
+ pad + j * real_tile_width,
609
+ min(pad + (j + 1) * real_tile_width, w),
610
+ pad + i * real_tile_height,
611
+ min(pad + (i + 1) * real_tile_height, h),
612
+ ]
613
+
614
+ # if the output bbox is close to the image boundary, we extend it to the image boundary
615
+ output_bbox = [
616
+ input_bbox[0] if input_bbox[0] > pad else 0,
617
+ input_bbox[1] if input_bbox[1] < w - pad else w,
618
+ input_bbox[2] if input_bbox[2] > pad else 0,
619
+ input_bbox[3] if input_bbox[3] < h - pad else h,
620
+ ]
621
+
622
+ # scale to get the final output bbox
623
+ output_bbox = [x * 8 if self.is_decoder else x // 8 for x in output_bbox]
624
+ tile_output_bboxes.append(output_bbox)
625
+
626
+ # indistinguishable expand the input bbox by pad pixels
627
+ tile_input_bboxes.append([
628
+ max(0, input_bbox[0] - pad),
629
+ min(w, input_bbox[1] + pad),
630
+ max(0, input_bbox[2] - pad),
631
+ min(h, input_bbox[3] + pad),
632
+ ])
633
+
634
+ return tile_input_bboxes, tile_output_bboxes
635
+
636
+ @torch.no_grad()
637
+ def estimate_group_norm(self, z, task_queue, color_fix):
638
+ device = z.device
639
+ tile = z
640
+ last_id = len(task_queue) - 1
641
+ while last_id >= 0 and task_queue[last_id][0] != 'pre_norm':
642
+ last_id -= 1
643
+ if last_id <= 0 or task_queue[last_id][0] != 'pre_norm':
644
+ raise ValueError('No group norm found in the task queue')
645
+ # estimate until the last group norm
646
+ for i in range(last_id + 1):
647
+ task = task_queue[i]
648
+ if task[0] == 'pre_norm':
649
+ group_norm_func = GroupNormParam.from_tile(tile, task[1])
650
+ task_queue[i] = ('apply_norm', group_norm_func)
651
+ if i == last_id:
652
+ return True
653
+ tile = group_norm_func(tile)
654
+ elif task[0] == 'store_res':
655
+ task_id = i + 1
656
+ while task_id < last_id and task_queue[task_id][0] != 'add_res':
657
+ task_id += 1
658
+ if task_id >= last_id:
659
+ continue
660
+ task_queue[task_id][1] = task[1](tile)
661
+ elif task[0] == 'add_res':
662
+ tile += task[1].to(device)
663
+ task[1] = None
664
+ elif color_fix and task[0] == 'downsample':
665
+ for j in range(i, last_id + 1):
666
+ if task_queue[j][0] == 'store_res':
667
+ task_queue[j] = ('store_res_cpu', task_queue[j][1])
668
+ return True
669
+ else:
670
+ tile = task[1](tile)
671
+ try:
672
+ devices.test_for_nans(tile, "vae")
673
+ except:
674
+ print(f'Nan detected in fast mode estimation. Fast mode disabled.')
675
+ return False
676
+
677
+ raise IndexError('Should not reach here')
678
+
679
+ # @perfcount
680
+ @torch.no_grad()
681
+ def vae_tile_forward(self, z):
682
+ """
683
+ Decode a latent vector z into an image in a tiled manner.
684
+ @param z: latent vector
685
+ @return: image
686
+ """
687
+ device = next(self.net.parameters()).device
688
+ net = self.net
689
+ tile_size = self.tile_size
690
+ is_decoder = self.is_decoder
691
+
692
+ z = z.detach() # detach the input to avoid backprop
693
+
694
+ N, height, width = z.shape[0], z.shape[2], z.shape[3]
695
+ net.last_z_shape = z.shape
696
+
697
+ # Split the input into tiles and build a task queue for each tile
698
+ print(f'[Tiled VAE]: input_size: {z.shape}, tile_size: {tile_size}, padding: {self.pad}')
699
+
700
+ in_bboxes, out_bboxes = self.split_tiles(height, width)
701
+
702
+ # Prepare tiles by split the input latents
703
+ tiles = []
704
+ for input_bbox in in_bboxes:
705
+ tile = z[:, :, input_bbox[2]:input_bbox[3], input_bbox[0]:input_bbox[1]].cpu()
706
+ tiles.append(tile)
707
+
708
+ num_tiles = len(tiles)
709
+ num_completed = 0
710
+
711
+ # Build task queues
712
+ single_task_queue = build_task_queue(net, is_decoder)
713
+ #print(single_task_queue)
714
+ if self.fast_mode:
715
+ # Fast mode: downsample the input image to the tile size,
716
+ # then estimate the group norm parameters on the downsampled image
717
+ scale_factor = tile_size / max(height, width)
718
+ z = z.to(device)
719
+ downsampled_z = F.interpolate(z, scale_factor=scale_factor, mode='nearest-exact')
720
+ # use nearest-exact to keep statictics as close as possible
721
+ print(f'[Tiled VAE]: Fast mode enabled, estimating group norm parameters on {downsampled_z.shape[3]} x {downsampled_z.shape[2]} image')
722
+
723
+ # ======= Special thanks to @Kahsolt for distribution shift issue ======= #
724
+ # The downsampling will heavily distort its mean and std, so we need to recover it.
725
+ std_old, mean_old = torch.std_mean(z, dim=[0, 2, 3], keepdim=True)
726
+ std_new, mean_new = torch.std_mean(downsampled_z, dim=[0, 2, 3], keepdim=True)
727
+ downsampled_z = (downsampled_z - mean_new) / std_new * std_old + mean_old
728
+ del std_old, mean_old, std_new, mean_new
729
+ # occasionally the std_new is too small or too large, which exceeds the range of float16
730
+ # so we need to clamp it to max z's range.
731
+ downsampled_z = torch.clamp_(downsampled_z, min=z.min(), max=z.max())
732
+ estimate_task_queue = clone_task_queue(single_task_queue)
733
+ if self.estimate_group_norm(downsampled_z, estimate_task_queue, color_fix=self.color_fix):
734
+ single_task_queue = estimate_task_queue
735
+ del downsampled_z
736
+
737
+ task_queues = [clone_task_queue(single_task_queue) for _ in range(num_tiles)]
738
+
739
+ # Dummy result
740
+ result = None
741
+ result_approx = None
742
+ #try:
743
+ # with devices.autocast():
744
+ # result_approx = torch.cat([F.interpolate(cheap_approximation(x).unsqueeze(0), scale_factor=opt_f, mode='nearest-exact') for x in z], dim=0).cpu()
745
+ #except: pass
746
+ # Free memory of input latent tensor
747
+ del z
748
+
749
+ # Task queue execution
750
+ pbar = tqdm(total=num_tiles * len(task_queues[0]), desc=f"[Tiled VAE]: Executing {'Decoder' if is_decoder else 'Encoder'} Task Queue: ")
751
+
752
+ # execute the task back and forth when switch tiles so that we always
753
+ # keep one tile on the GPU to reduce unnecessary data transfer
754
+ forward = True
755
+ interrupted = False
756
+ #state.interrupted = interrupted
757
+ while True:
758
+ #if state.interrupted: interrupted = True ; break
759
+
760
+ group_norm_param = GroupNormParam()
761
+ for i in range(num_tiles) if forward else reversed(range(num_tiles)):
762
+ #if state.interrupted: interrupted = True ; break
763
+
764
+ tile = tiles[i].to(device)
765
+ input_bbox = in_bboxes[i]
766
+ task_queue = task_queues[i]
767
+
768
+ interrupted = False
769
+ while len(task_queue) > 0:
770
+ #if state.interrupted: interrupted = True ; break
771
+
772
+ # DEBUG: current task
773
+ # print('Running task: ', task_queue[0][0], ' on tile ', i, '/', num_tiles, ' with shape ', tile.shape)
774
+ task = task_queue.pop(0)
775
+ if task[0] == 'pre_norm':
776
+ group_norm_param.add_tile(tile, task[1])
777
+ break
778
+ elif task[0] == 'store_res' or task[0] == 'store_res_cpu':
779
+ task_id = 0
780
+ res = task[1](tile)
781
+ if not self.fast_mode or task[0] == 'store_res_cpu':
782
+ res = res.cpu()
783
+ while task_queue[task_id][0] != 'add_res':
784
+ task_id += 1
785
+ task_queue[task_id][1] = res
786
+ elif task[0] == 'add_res':
787
+ tile += task[1].to(device)
788
+ task[1] = None
789
+ else:
790
+ tile = task[1](tile)
791
+ pbar.update(1)
792
+
793
+ if interrupted: break
794
+
795
+ # check for NaNs in the tile.
796
+ # If there are NaNs, we abort the process to save user's time
797
+ #devices.test_for_nans(tile, "vae")
798
+
799
+ #print(tiles[i].shape, tile.shape, i, num_tiles)
800
+ if len(task_queue) == 0:
801
+ tiles[i] = None
802
+ num_completed += 1
803
+ if result is None: # NOTE: dim C varies from different cases, can only be inited dynamically
804
+ result = torch.zeros((N, tile.shape[1], height * 8 if is_decoder else height // 8, width * 8 if is_decoder else width // 8), device=device, requires_grad=False)
805
+ result[:, :, out_bboxes[i][2]:out_bboxes[i][3], out_bboxes[i][0]:out_bboxes[i][1]] = crop_valid_region(tile, in_bboxes[i], out_bboxes[i], is_decoder)
806
+ del tile
807
+ elif i == num_tiles - 1 and forward:
808
+ forward = False
809
+ tiles[i] = tile
810
+ elif i == 0 and not forward:
811
+ forward = True
812
+ tiles[i] = tile
813
+ else:
814
+ tiles[i] = tile.cpu()
815
+ del tile
816
+
817
+ if interrupted: break
818
+ if num_completed == num_tiles: break
819
+
820
+ # insert the group norm task to the head of each task queue
821
+ group_norm_func = group_norm_param.summary()
822
+ if group_norm_func is not None:
823
+ for i in range(num_tiles):
824
+ task_queue = task_queues[i]
825
+ task_queue.insert(0, ('apply_norm', group_norm_func))
826
+
827
+ # Done!
828
+ pbar.close()
829
+ return result if result is not None else result_approx.to(device)
video.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Render the zoom levels into one continuous zoom clip.
2
+
3
+ Each segment pushes in on level i until its frame is exactly the blurry crop that level i+1
4
+ was made from, then holds still and cross-fades blurry into sharp. The hold is the point:
5
+ it is where the viewer sees what the model actually added.
6
+ """
7
+ import tempfile
8
+
9
+ import imageio.v2 as imageio
10
+ import numpy as np
11
+ from PIL import Image
12
+
13
+ from geometry import window_rect
14
+
15
+ FPS = 30
16
+ PUSH_FRAMES = 30
17
+ FADE_FRAMES = 14
18
+ TAIL_FRAMES = 24
19
+
20
+
21
+ def _ease(t):
22
+ return t * t * (3 - 2 * t)
23
+
24
+
25
+ def _lerp_rect(a, b, t):
26
+ return tuple(round(p + (q - p) * t) for p, q in zip(a, b))
27
+
28
+
29
+ def _frames(levels, upscale, center):
30
+ size = levels[0].size
31
+ full = (0, 0, size[0], size[1])
32
+ for i in range(len(levels) - 1):
33
+ target = window_rect(size, upscale, center)
34
+ for f in range(PUSH_FRAMES):
35
+ # f / (PUSH_FRAMES - 1), not f / PUSH_FRAMES: the last frame has to land exactly on
36
+ # `target`, which is the crop the next level was built from. Otherwise it jumps.
37
+ rect = _lerp_rect(full, target, _ease(f / (PUSH_FRAMES - 1)))
38
+ yield levels[i].crop(rect).resize(size, Image.BICUBIC)
39
+ blurry = np.asarray(levels[i].crop(target).resize(size, Image.BICUBIC), dtype=np.float32)
40
+ sharp = np.asarray(levels[i + 1], dtype=np.float32)
41
+ for f in range(FADE_FRAMES):
42
+ a = _ease((f + 1) / FADE_FRAMES)
43
+ yield Image.fromarray((blurry * (1 - a) + sharp * a).astype(np.uint8))
44
+ for _ in range(TAIL_FRAMES):
45
+ yield levels[-1]
46
+
47
+
48
+ def render(levels, upscale=4, center=(0.5, 0.5)):
49
+ """Write an mp4 of the zoom and return its path. Needs at least two levels."""
50
+ if len(levels) < 2:
51
+ return None
52
+ path = tempfile.mkstemp(suffix=".mp4")[1]
53
+ with imageio.get_writer(path, fps=FPS, codec="libx264", quality=8,
54
+ pixelformat="yuv420p", macro_block_size=1) as out:
55
+ for frame in _frames(levels, upscale, center):
56
+ out.append_data(np.asarray(frame.convert("RGB")))
57
+ return path
zoom.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OracleZoom recursive zoom, one image at a time, all in memory.
2
+
3
+ Flattened from src/opd_zoom/teacher/oracle_infer.py for the Space. Three differences, none
4
+ of them to the model: no disk round-trip between recursions, the zoom window can sit
5
+ anywhere instead of only the centre, and the loop is a generator so the UI can show each
6
+ level the moment it lands. Everything on one device, which is what ZeroGPU gives us.
7
+ """
8
+ import os
9
+ import sys
10
+
11
+ import torch
12
+ from PIL import Image
13
+ from torchvision import transforms
14
+
15
+ from geometry import PROCESS_SIZE, resize_and_center_crop, zoom_window
16
+
17
+ VENDOR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vendor")
18
+ if VENDOR not in sys.path:
19
+ sys.path.insert(0, VENDOR)
20
+
21
+ REPO = "dipta007/OracleZoom"
22
+ SD3 = "stabilityai/stable-diffusion-3-medium-diffusers"
23
+ VLM = "Qwen/Qwen2.5-VL-3B-Instruct"
24
+ COZ_PROMPT = (
25
+ "The second image is a zoom-in of the first image. Based on this knowledge, "
26
+ "what is in the second image? Give me a set of words."
27
+ )
28
+ _to_tensor = transforms.Compose([transforms.ToTensor()])
29
+
30
+
31
+ class _SRArgs:
32
+ def __init__(self, coz_ckpt):
33
+ self.lora_path = f"{coz_ckpt}/SR_LoRA/model_20001.pkl"
34
+ self.vae_path = f"{coz_ckpt}/SR_VAE/vae_encoder_20001.pt"
35
+ self.pretrained_model_name_or_path = SD3
36
+ self.process_size = PROCESS_SIZE
37
+ self.lora_rank = 4
38
+ self.merge_and_unload_lora = False
39
+ self.mixed_precision = "fp16"
40
+
41
+
42
+ class Models:
43
+ """Built once at import, read-only afterwards. Concurrent requests only read."""
44
+
45
+ def __init__(self, weights=None):
46
+ from huggingface_hub import snapshot_download
47
+
48
+ w = weights or snapshot_download(repo_id=REPO)
49
+ self.vlm, self.proc, self.process_vision_info = _build_vlm(f"{w}/ckpt/VLM_LoRA/checkpoint-10000")
50
+ self.sr = _build_sr(f"{w}/ckpt", f"{w}/merged_transformer.safetensors")
51
+
52
+
53
+ def _build_vlm(lora_path):
54
+ from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
55
+ from qwen_vl_utils import process_vision_info
56
+ from peft import PeftModel
57
+
58
+ # device_map="auto" asks accelerate to read real GPU memory, which does not exist at
59
+ # module scope on ZeroGPU. Place it ourselves. sdpa because flash-attn has no wheel here.
60
+ model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
61
+ VLM, torch_dtype=torch.bfloat16, attn_implementation="sdpa"
62
+ )
63
+ model = PeftModel.from_pretrained(model, lora_path).merge_and_unload()
64
+ return model.eval().to("cuda"), AutoProcessor.from_pretrained(VLM), process_vision_info
65
+
66
+
67
+ def _build_sr(coz_ckpt, merged_transformer):
68
+ from safetensors.torch import load_file
69
+ from osediff_sd3 import OSEDiff_SD3_TEST, SD3Euler
70
+
71
+ sr = SD3Euler()
72
+ for m in (sr.text_enc_1, sr.text_enc_2, sr.text_enc_3):
73
+ m.to("cuda")
74
+ sr.transformer.to("cuda", dtype=torch.float32)
75
+ sr.vae.to("cuda", dtype=torch.float32)
76
+ for m in (sr.text_enc_1, sr.text_enc_2, sr.text_enc_3, sr.transformer, sr.vae):
77
+ m.requires_grad_(False)
78
+ # Construct first, load second. OSEDiff_SD3_TEST swaps every targeted Linear for a
79
+ # LoraInjectedLinear, which renames the keys. Our merged checkpoint was saved after that
80
+ # swap, so loading it earlier matches nothing and silently leaves the base transformer.
81
+ test = OSEDiff_SD3_TEST(_SRArgs(coz_ckpt), sr)
82
+ sd = load_file(merged_transformer)
83
+ missing, unexpected = test.model.transformer.load_state_dict(
84
+ {k: v.to(torch.float32) for k, v in sd.items()}, strict=False)
85
+ if len(unexpected) > len(sd) // 2:
86
+ raise RuntimeError(f"merged transformer did not match: {len(unexpected)} of {len(sd)} "
87
+ f"keys unexpected. LoRA injection order or checkpoint is wrong.")
88
+ print(f"#### merged transformer loaded (missing {len(missing)} unexpected {len(unexpected)})")
89
+ return test
90
+
91
+
92
+ def write_prompt(models, first, second, max_new_tokens=32):
93
+ messages = [
94
+ {"role": "system", "content": COZ_PROMPT},
95
+ {"role": "user", "content": [{"type": "image", "image": first},
96
+ {"type": "image", "image": second}]},
97
+ ]
98
+ text = models.proc.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
99
+ images, videos = models.process_vision_info(messages)
100
+ inputs = models.proc(text=[text], images=images, videos=videos,
101
+ padding=True, return_tensors="pt").to("cuda")
102
+ gen = models.vlm.generate(**inputs, max_new_tokens=max_new_tokens)
103
+ trimmed = [o[len(i):] for i, o in zip(inputs.input_ids, gen)]
104
+ return models.proc.batch_decode(trimmed, skip_special_tokens=True,
105
+ clean_up_tokenization_spaces=False)[0].strip()
106
+
107
+
108
+ def super_resolve(models, img, prompt):
109
+ lq = _to_tensor(img).unsqueeze(0).to("cuda") * 2 - 1
110
+ with torch.no_grad():
111
+ out = torch.clamp(models.sr(lq, prompt=prompt)[0].cpu(), -1.0, 1.0)
112
+ return transforms.ToPILImage()(out * 0.5 + 0.5)
113
+
114
+
115
+ def zoom(models, image, levels=4, upscale=4, center=(0.5, 0.5)):
116
+ """Yield one (level, factor, prompt, blurry_input, result) per recursion.
117
+
118
+ `blurry_input` is the plain bicubic enlargement the model starts from. It is what the
119
+ super-resolution has to improve on, so it doubles as the honest before-picture.
120
+ """
121
+ cur = resize_and_center_crop(image)
122
+ yield 0, 1, "", cur, cur
123
+ for i in range(levels):
124
+ blurry = zoom_window(cur, upscale, center).resize(cur.size, Image.BICUBIC)
125
+ prompt = write_prompt(models, cur, blurry)
126
+ cur = super_resolve(models, blurry, prompt)
127
+ yield i + 1, upscale ** (i + 1), prompt, blurry, cur