multimodalart HF Staff commited on
Commit
fddbcbc
·
verified ·
1 Parent(s): f987052

Redirect to the authors' Space dipta007/OracleZoom

Browse files
LICENSE-OracleZoom DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Shubhashis Roy Dipta, Sourajit Saha, Shaswati Saha, Nobin Sarwar
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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -3,64 +3,24 @@ title: OracleZoom Super Resolution
3
  emoji: 🔎
4
  colorFrom: purple
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
  license: mit
12
- short_description: Recursive 4x zoom to 256x with OracleZoom
13
  tags:
14
  - super-resolution
15
  - image-to-image
16
- - diffusion
17
- - zoom
18
  models:
19
  - dipta007/OracleZoom
20
- - stabilityai/stable-diffusion-3-medium-diffusers
21
- - Qwen/Qwen2.5-VL-3B-Instruct
22
- datasets:
23
- - dipta007/OracleZoom-4KLSDB-train
24
  ---
25
 
26
- # OracleZoom 🔎
27
 
28
- Upload a photo, pick a point, and zoom into it 4x at a time. Each step super-resolves the
29
- *previous step's own output*, so four steps reach **256x**. A Qwen2.5-VL prompter reads the
30
- pair (current level, next blurry crop) and writes the conditioning for the SD3-medium
31
- one-step SR transformer that OracleZoom fine-tuned.
32
 
33
  - Paper: https://huggingface.co/papers/2609.06490
34
  - Code: https://github.com/dipta007/OracleZoom
35
  - Project page: https://dipta007.github.io/OracleZoom/
36
  - Model: https://huggingface.co/dipta007/OracleZoom
37
- - Training data: https://huggingface.co/datasets/dipta007/OracleZoom-4KLSDB-train
38
- - Authors' own Space: https://huggingface.co/spaces/dipta007/OracleZoom
39
-
40
- ## What is in here
41
-
42
- `app.py` is the interface. The pipeline itself is the authors' inference path, reused
43
- unchanged from `space/` in [dipta007/OracleZoom](https://github.com/dipta007/OracleZoom)
44
- (MIT, see `LICENSE-OracleZoom`): `zoom.py` is the recursion, `geometry.py` the crop
45
- geometry, `video.py` the clip renderer, and `vendor/` holds the four files copied from
46
- [Chain-of-Zoom](https://github.com/bryanswkim/Chain-of-Zoom) (MIT, see `vendor/COZ-LICENSE`).
47
-
48
- The example images in `samples/` are the authors' own, shipped in the MIT-licensed repo and
49
- drawn from their [4KLSDB set](https://huggingface.co/datasets/dipta007/OracleZoom-4KLSDB-train)
50
- (CC-BY-4.0) — credit to Shubhashis Roy Dipta, Sourajit Saha, Shaswati Saha and Nobin Sarwar.
51
-
52
- ## Notes for anyone duplicating this
53
-
54
- - Needs an `HF_TOKEN` secret: Stable Diffusion 3-medium is gated, so the token must come from
55
- an account that accepted its licence. `huggingface_hub` picks the variable up on its own.
56
- - Hardware: ZeroGPU. The pipeline holds roughly 27 GB of weights, inside the 48 GB a `large`
57
- slice gives. Boot streams SD3-medium, Qwen2.5-VL-3B and the merged transformer, so the
58
- startup timeout is raised to 45 minutes.
59
- - Gradio is pinned to the 5.x series on purpose: `transformers==4.49.0` (what this pipeline
60
- was built against) requires `huggingface-hub<1.0`, which gradio 6 cannot satisfy.
61
-
62
- ## Honest limits
63
-
64
- Past the first step or two there is no ground truth left to recover, so the deep levels are
65
- plausible detail rather than measured detail. The zoom is a crop of your photo, not a real
66
- lens moving closer. The paper is explicit about which claims it does and does not make.
 
3
  emoji: 🔎
4
  colorFrom: purple
5
  colorTo: gray
6
+ sdk: static
7
+ app_file: index.html
8
+ pinned: false
 
 
9
  license: mit
10
+ short_description: Moved — use the authors' Space dipta007/OracleZoom
11
  tags:
12
  - super-resolution
13
  - image-to-image
 
 
14
  models:
15
  - dipta007/OracleZoom
 
 
 
 
16
  ---
17
 
18
+ # OracleZoom has moved 🔎
19
 
20
+ This Space is now a redirect to the authors' own demo:
21
+ **https://huggingface.co/spaces/dipta007/OracleZoom**
 
 
22
 
23
  - Paper: https://huggingface.co/papers/2609.06490
24
  - Code: https://github.com/dipta007/OracleZoom
25
  - Project page: https://dipta007.github.io/OracleZoom/
26
  - Model: https://huggingface.co/dipta007/OracleZoom
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py DELETED
@@ -1,271 +0,0 @@
1
- """OracleZoom — reference-constrained recursive image super-resolution.
2
-
3
- Upload a photo, pick a point, and zoom into it 4x at a time. Each step super-resolves the
4
- previous step's own output, so four steps reach 256x. A Qwen2.5-VL prompter reads the pair
5
- (current level, next blurry crop) and writes the conditioning for the SD3 one-step SR model.
6
-
7
- The pipeline is the authors' own inference path, taken from
8
- https://github.com/dipta007/OracleZoom (MIT): `zoom.py`, `geometry.py`, `video.py` and
9
- `vendor/` are theirs; only the interface below is new.
10
- """
11
- import os
12
- import time
13
-
14
- # High-res pixel-space VAE decodes spike the allocator on ZeroGPU. Must precede any torch import.
15
- os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
16
- # The VLM prompter tokenizes in the main process and every request forks a GPU worker.
17
- os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
18
-
19
- import spaces # noqa: E402 — before anything that touches torch/CUDA
20
- import gradio as gr # noqa: E402
21
- from PIL import Image, ImageDraw # noqa: E402
22
-
23
- import geometry # noqa: E402
24
- import video # noqa: E402
25
- import zoom # noqa: E402
26
-
27
- UPSCALE = 4
28
- DEFAULT_STEPS = 4
29
- MAX_STEPS = 8
30
- ACCENT = "#a855f7"
31
- SAMPLES = ["0479", "0064", "0245", "0393", "0457"]
32
-
33
- # A real run, shipped with the Space, so the page shows the payoff before anyone spends quota.
34
- EXAMPLE_INPUT = "samples/example_1x.png"
35
- EXAMPLE_CLIP = "samples/example_zoom.mp4"
36
- EXAMPLE_LEVELS = [(EXAMPLE_INPUT, "input")] + \
37
- [(f"samples/example_{f}x.png", f"{f}x") for f in (4, 16, 64, 256)]
38
- EXAMPLE_COMPARE = ("samples/example_256x_input.png", "samples/example_256x.png")
39
- EXAMPLE_NOTE = "_From the shipped example run. Zoom your own photo to see its prompts._"
40
-
41
- MODELS = zoom.Models()
42
-
43
- HEADER = """# OracleZoom 🔎
44
-
45
- Zoom into a photo far past what it holds. Every step is a 4x super-resolution of the
46
- *previous step's own output*, so four steps reach **256x** — and the last available
47
- reference keeps each step honest about the structure it can still verify.
48
-
49
- [Paper](https://huggingface.co/papers/2609.06490) ·
50
- [Code](https://github.com/dipta007/OracleZoom) ·
51
- [Project page](https://dipta007.github.io/OracleZoom/) ·
52
- [Model](https://huggingface.co/dipta007/OracleZoom) ·
53
- [Training data](https://huggingface.co/datasets/dipta007/OracleZoom-4KLSDB-train)
54
- """
55
-
56
- BIBTEX = """@misc{dipta2026oraclezoomonpolicyselfdistillationinspired,
57
- title = {OracleZoom: On-Policy Self-Distillation Inspired Reference-Constrained Recursive Image Super Resolution},
58
- author = {Shubhashis Roy Dipta and Sourajit Saha and Shaswati Saha and Nobin Sarwar},
59
- year = {2026},
60
- eprint = {2609.06490},
61
- archivePrefix = {arXiv},
62
- primaryClass = {cs.CV},
63
- url = {https://arxiv.org/abs/2609.06490}
64
- }"""
65
-
66
- CSS = """
67
- #col-container {max-width: 1400px; margin: 0 auto;}
68
- .dark .gradio-container {color: var(--body-text-color);}
69
- #zoom-target img {cursor: crosshair;}
70
- #clip video {border-radius: 12px;}
71
- /* The level strip should hug its content: .gallery-container carries the height, so styling
72
- only the outer block leaves a dead band below the thumbnails. */
73
- #levels, #levels .gallery-container, #levels .grid-wrap {
74
- height: auto !important; max-height: none !important; min-height: 0 !important;
75
- overflow: visible !important; flex-grow: 0 !important;}
76
- """
77
-
78
-
79
- def label(factor: int) -> str:
80
- return "input" if factor == 1 else f"{factor}x"
81
-
82
-
83
- def scale_labels(levels: int) -> list:
84
- return [label(UPSCALE ** n) for n in range(1, int(levels) + 1)]
85
-
86
-
87
- def preview(image, levels, cx, cy):
88
- """Draw the nested zoom windows so nobody spends a run to find out where it lands.
89
-
90
- Falls back to the shipped example when nothing is uploaded yet — returning None would
91
- blank the box the moment a visitor touched a slider.
92
- """
93
- canvas = geometry.resize_and_center_crop(
94
- image if image is not None else Image.open(EXAMPLE_INPUT)).convert("RGB")
95
- rects = geometry.nested_rects(canvas.size, int(levels), UPSCALE, (cx, cy))
96
- dimmed = Image.blend(canvas, Image.new("RGB", canvas.size, (0, 0, 0)), 0.45)
97
- if rects:
98
- dimmed.paste(canvas.crop(rects[0]), rects[0][:2])
99
- draw = ImageDraw.Draw(dimmed)
100
- for rect in rects:
101
- if rect[2] - rect[0] >= 3:
102
- draw.rectangle(rect, outline=ACCENT, width=2)
103
- return dimmed
104
-
105
-
106
- def pick_point(evt: gr.SelectData):
107
- """Move the zoom point to wherever the preview was clicked (rounded to the slider step)."""
108
- x, y = evt.index
109
- return (round(x / geometry.PROCESS_SIZE, 2), round(y / geometry.PROCESS_SIZE, 2))
110
-
111
-
112
- def gallery_shape(levels):
113
- """One row up to five levels, two beyond. Height is left to CSS, which hugs the content."""
114
- items = int(levels) + 1
115
- rows = 1 if items <= 5 else 2
116
- return gr.update(columns=-(-items // rows), rows=rows)
117
-
118
-
119
- def _gallery_items(gallery):
120
- """Gradio hands a filepath-typed Gallery back to a handler as (path, caption) tuples."""
121
- return [(item[0], item[1] or "") for item in gallery or []
122
- if isinstance(item, (tuple, list)) and len(item) == 2]
123
-
124
-
125
- def compare_level(gallery, cx, cy, evt: gr.SelectData):
126
- """Rebuild the before/after for whichever level was clicked in the strip.
127
-
128
- The left side is the plain bicubic enlargement that fed that step; recomputing it from the
129
- levels already on screen keeps this on the CPU, so browsing costs no GPU quota.
130
- """
131
- items = _gallery_items(gallery)
132
- index = getattr(evt, "index", None)
133
- i = index if isinstance(index, int) else None
134
- if i is None or i <= 0 or i >= len(items):
135
- return gr.skip()
136
- prev = Image.open(items[i - 1][0]).convert("RGB")
137
- blurry = geometry.zoom_window(prev, UPSCALE, (cx, cy)).resize(prev.size, Image.BICUBIC)
138
- return blurry, Image.open(items[i][0]).convert("RGB")
139
-
140
-
141
- def estimate_duration(image=None, levels=DEFAULT_STEPS, *args, **kwargs):
142
- """GPU seconds to reserve. Checked against the visitor's remaining quota before the run,
143
- so a loose number locks out low-quota visitors and a tight one gets the run killed.
144
-
145
- Measured on this Space (warm worker, 512x512): 3.8 s at 1 step, 7.9 s at 4, 15.6 s at 8
146
- — about 2 s fixed plus 1.8 s per step, clip rendering included. Doubled, which is what
147
- covers a cold worker streaming the ~27 GB of weights into VRAM first.
148
- """
149
- return int(6 + 3 * int(levels or DEFAULT_STEPS))
150
-
151
-
152
- def _zoom_stream(image, levels, cx, cy):
153
- if image is None:
154
- raise gr.Error("Upload a photo first (or click one of the examples).")
155
- levels = int(levels)
156
- started = time.perf_counter()
157
- gallery, prompts, frames, compare = [], [], [], None
158
- for step, factor, prompt, blurry, result in zoom.zoom(MODELS, image, levels, UPSCALE,
159
- (cx, cy)):
160
- frames.append(result)
161
- gallery.append((result, label(factor)))
162
- if step:
163
- compare = (blurry, result)
164
- prompts.append(f"**{label(factor)}** &nbsp; {prompt or '_(no prompt)_'}")
165
- left = levels - step
166
- if not step:
167
- status = f"Warmed up. Zooming {levels} step{'s' if levels > 1 else ''}…"
168
- elif left:
169
- status = f"At **{label(factor)}**, {left} step{'s' if left > 1 else ''} to go…"
170
- else:
171
- status = f"At **{label(factor)}**. Rendering the clip…"
172
- # The first yield clears any clip left from the previous run; later ones leave it alone.
173
- yield (status, (None if not step else gr.skip()), gallery, compare,
174
- "\n\n".join(prompts))
175
- clip = video.render(frames, UPSCALE, (cx, cy))
176
- elapsed = time.perf_counter() - started
177
- print(f"#### zoom: {levels} step(s) in {elapsed:.1f}s", flush=True)
178
- yield (f"Done. Reached **{label(UPSCALE ** levels)}** in {elapsed:.0f}s. "
179
- f"Click any level below to compare it with the plain enlargement it started from.",
180
- clip, gallery, compare, "\n\n".join(prompts))
181
-
182
-
183
- @spaces.GPU(duration=estimate_duration)
184
- def zoom_in(image, levels: int = DEFAULT_STEPS, cx: float = 0.5, cy: float = 0.5):
185
- """Recursively super-resolve an image, 4x per step, streaming each level as it lands.
186
-
187
- Args:
188
- image: the photo to zoom into (resized and centre-cropped to 512x512 first).
189
- levels: how many 4x steps to take. 4 steps reach 256x.
190
- cx: horizontal position of the zoom point, 0 (left) to 1 (right).
191
- cy: vertical position of the zoom point, 0 (top) to 1 (bottom).
192
- """
193
- yield from _zoom_stream(image, levels, cx, cy)
194
-
195
-
196
- @spaces.GPU(duration=estimate_duration)
197
- def zoom_in_cached(image, levels: int = DEFAULT_STEPS, cx: float = 0.5, cy: float = 0.5):
198
- """Same zoom, returned in one shot so `gr.Examples` can cache it.
199
-
200
- Args:
201
- image: the photo to zoom into.
202
- levels: how many 4x steps to take.
203
- cx: horizontal position of the zoom point, 0 to 1.
204
- cy: vertical position of the zoom point, 0 to 1.
205
- """
206
- final = None
207
- for final in _zoom_stream(image, levels, cx, cy):
208
- pass
209
- return final
210
-
211
-
212
- with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="OracleZoom: zoom past 256x") as demo:
213
- with gr.Column(elem_id="col-container"):
214
- gr.Markdown(HEADER)
215
- with gr.Row(equal_height=False):
216
- with gr.Column(scale=4):
217
- image = gr.Image(label="Your photo", type="pil", height=260,
218
- sources=["upload", "clipboard"])
219
- # Seeded with the shipped example so the overlay explains itself before any upload.
220
- target = gr.Image(value=preview(None, DEFAULT_STEPS, 0.5, 0.5),
221
- elem_id="zoom-target", label="Click to move the zoom point",
222
- type="pil", interactive=False, height=330,
223
- show_download_button=False)
224
- levels = gr.Slider(1, MAX_STEPS, value=DEFAULT_STEPS, step=1, label="Zoom steps",
225
- info=f"Each step is {UPSCALE}x. 4 steps reach 256x, "
226
- f"{MAX_STEPS} reach {UPSCALE ** MAX_STEPS}x. "
227
- f"Fewer steps finish sooner.")
228
- go = gr.Button("🔎 Zoom in", variant="primary", size="lg")
229
- status = gr.Markdown("Showing a shipped example run. "
230
- "Upload a photo, then press **Zoom in**.")
231
- with gr.Accordion("Advanced settings", open=False):
232
- cx = gr.Slider(0, 1, value=0.5, step=0.01, label="Zoom point — horizontal")
233
- cy = gr.Slider(0, 1, value=0.5, step=0.01, label="Zoom point — vertical")
234
- gr.Markdown("##### What the VLM prompter read at each step")
235
- prompts = gr.Markdown(EXAMPLE_NOTE)
236
- with gr.Column(scale=6):
237
- clip = gr.Video(value=EXAMPLE_CLIP, label="The zoom", elem_id="clip",
238
- autoplay=True, loop=True, height=460)
239
- gallery = gr.Gallery(value=EXAMPLE_LEVELS, label="Every level, start to finish",
240
- elem_id="levels", columns=5, rows=1, object_fit="cover",
241
- allow_preview=False, show_download_button=True)
242
- compare = gr.ImageSlider(value=EXAMPLE_COMPARE, height=470, elem_id="compare",
243
- label="Plain enlargement (left) vs OracleZoom (right)")
244
-
245
- gr.Examples(
246
- examples=[f"samples/{n}.png" for n in SAMPLES],
247
- inputs=[image],
248
- outputs=[status, clip, gallery, compare, prompts],
249
- fn=zoom_in_cached,
250
- cache_examples=True,
251
- cache_mode="lazy",
252
- label="The authors' own example photos — click one to zoom it to 256x",
253
- )
254
-
255
- gr.Markdown("**Honest limits.** Past the first step or two there is no ground truth "
256
- "left to recover, so deep levels are plausible detail, not measured "
257
- "detail. The zoom is a crop of your photo, not a lens moving closer.")
258
- gr.Markdown("### Cite")
259
- gr.Code(value=BIBTEX, language=None, show_label=False, container=False)
260
-
261
- controls = [image, levels, cx, cy]
262
- for control in controls:
263
- control.change(preview, controls, target, show_api=False)
264
- target.select(pick_point, None, [cx, cy], show_api=False)
265
- # Reshape before the run, not after, so the strip does not resize under the results.
266
- levels.change(gallery_shape, levels, gallery, show_api=False)
267
- go.click(zoom_in, controls, [status, clip, gallery, compare, prompts], api_name="zoom_in")
268
- gallery.select(compare_level, [gallery, cx, cy], compare, show_api=False)
269
-
270
- if __name__ == "__main__":
271
- demo.queue(max_size=20).launch(mcp_server=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
geometry.py DELETED
@@ -1,45 +0,0 @@
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
-
33
- Stops early once a window would be under a pixel wide. The recursion itself never
34
- degenerates, because every step resizes its crop back up to the full canvas, but drawing
35
- all those steps onto the one original canvas does run out of pixels.
36
- """
37
- rects, rect = [], (0, 0, size[0], size[1])
38
- for _ in range(levels):
39
- side = rect[2] - rect[0]
40
- if side // upscale < 1:
41
- break
42
- l, t, r, b = window_rect((side, side), upscale, center)
43
- rect = (rect[0] + l, rect[1] + t, rect[0] + r, rect[1] + b)
44
- rects.append(rect)
45
- return rects
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.html ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>OracleZoom has moved</title>
7
+ <link rel="canonical" href="https://huggingface.co/spaces/dipta007/OracleZoom">
8
+ <style>
9
+ :root {
10
+ color-scheme: light dark;
11
+ --bg: #ffffff;
12
+ --fg: #111827;
13
+ --muted: #6b7280;
14
+ --line: #e5e7eb;
15
+ --accent: #ff9d00;
16
+ }
17
+ @media (prefers-color-scheme: dark) {
18
+ :root { --bg: #0b0f19; --fg: #f3f4f6; --muted: #9ca3af; --line: #1f2937; }
19
+ }
20
+ * { box-sizing: border-box; }
21
+ body {
22
+ margin: 0;
23
+ min-height: 100vh;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ padding: 24px;
28
+ background: var(--bg);
29
+ color: var(--fg);
30
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
31
+ line-height: 1.55;
32
+ }
33
+ main { max-width: 34rem; text-align: center; }
34
+ .emoji { font-size: 3rem; line-height: 1; }
35
+ h1 { font-size: 1.5rem; font-weight: 650; margin: 1rem 0 0.5rem; }
36
+ p { color: var(--muted); margin: 0 0 1.5rem; }
37
+ a.cta {
38
+ display: inline-block;
39
+ padding: 0.75rem 1.5rem;
40
+ border-radius: 0.65rem;
41
+ background: var(--accent);
42
+ color: #111827;
43
+ font-weight: 600;
44
+ text-decoration: none;
45
+ }
46
+ a.cta:hover { filter: brightness(1.05); }
47
+ .links {
48
+ margin-top: 2rem;
49
+ padding-top: 1.25rem;
50
+ border-top: 1px solid var(--line);
51
+ font-size: 0.875rem;
52
+ color: var(--muted);
53
+ }
54
+ .links a { color: inherit; }
55
+ .count { font-variant-numeric: tabular-nums; }
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <main>
60
+ <div class="emoji">🔎</div>
61
+ <h1>OracleZoom lives on the authors' Space</h1>
62
+ <p>
63
+ Redirecting to <strong>dipta007/OracleZoom</strong> in <span class="count" id="count">3</span>s.
64
+ </p>
65
+ <a class="cta" id="go" href="https://huggingface.co/spaces/dipta007/OracleZoom" target="_top" rel="noopener">
66
+ Open OracleZoom →
67
+ </a>
68
+ <div class="links">
69
+ <a href="https://huggingface.co/papers/2609.06490" target="_blank" rel="noopener">Paper</a> ·
70
+ <a href="https://github.com/dipta007/OracleZoom" target="_blank" rel="noopener">Code</a> ·
71
+ <a href="https://dipta007.github.io/OracleZoom/" target="_blank" rel="noopener">Project page</a> ·
72
+ <a href="https://huggingface.co/dipta007/OracleZoom" target="_blank" rel="noopener">Model</a>
73
+ </div>
74
+ </main>
75
+ <script>
76
+ const TARGET = "https://huggingface.co/spaces/dipta007/OracleZoom";
77
+ const el = document.getElementById("count");
78
+ let n = 3;
79
+ const tick = setInterval(() => {
80
+ n -= 1;
81
+ if (n > 0) { el.textContent = n; return; }
82
+ clearInterval(tick);
83
+ el.textContent = 0;
84
+ try {
85
+ if (window.top !== window.self) { window.top.location.replace(TARGET); }
86
+ else { window.location.replace(TARGET); }
87
+ } catch (e) {
88
+ document.getElementById("go").focus();
89
+ }
90
+ }, 1000);
91
+ </script>
92
+ </body>
93
+ </html>
requirements.txt DELETED
@@ -1,28 +0,0 @@
1
- # Pins taken from the authors' own Space (github.com/dipta007/OracleZoom, space/requirements.txt).
2
- # diffusers stays at the paper's version: the SR LoRA is injected by matching the class name
3
- # "AdaLayerNormZero", so a diffusers rename would silently inject nothing.
4
- # transformers 4.49.0 is what the Qwen2.5-VL prompter + peft merge path was built against; it
5
- # also pins huggingface-hub<1.0, which is why the Space runs gradio 5.50 rather than 6.x.
6
- # gradio / spaces / huggingface_hub are deliberately absent (platform-managed).
7
- torch==2.8.0
8
- torchvision==0.23.0
9
- diffusers==0.32.2
10
- transformers==4.49.0
11
- accelerate
12
- peft>=0.13
13
- safetensors
14
- qwen-vl-utils
15
- # SD3's third text encoder is T5. Its fast tokenizer is built from a sentencepiece model, and
16
- # the conversion wants protobuf. Both arrive transitively in the research env, not here.
17
- sentencepiece
18
- protobuf
19
- lpips
20
- einops
21
- pyyaml
22
- tqdm
23
- numpy
24
- pillow
25
- imageio
26
- imageio-ffmpeg
27
- # demo.launch(mcp_server=True)
28
- mcp
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
samples/0064.png DELETED

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

  • SHA256: e6129a5d9a222aae938a3fc639b63e7039b8e5ae9c2561fd8d93ba6128b614f3
  • Pointer size: 131 Bytes
  • Size of remote file: 406 kB
samples/example_zoom.mp4 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:dc9b6172522eb830313cf1f18f9bb852706886005850dffd80e45a66f91391c7
3
- size 3691325
 
 
 
 
vendor/COZ-LICENSE DELETED
@@ -1,21 +0,0 @@
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 DELETED
@@ -1,137 +0,0 @@
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 DELETED
@@ -1,61 +0,0 @@
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 DELETED
@@ -1,913 +0,0 @@
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 DELETED
@@ -1,138 +0,0 @@
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 DELETED
@@ -1,829 +0,0 @@
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 DELETED
@@ -1,57 +0,0 @@
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 DELETED
@@ -1,138 +0,0 @@
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 gc
9
- import os
10
- import sys
11
-
12
- import torch
13
- from PIL import Image
14
- from torchvision import transforms
15
-
16
- from geometry import PROCESS_SIZE, resize_and_center_crop, zoom_window
17
-
18
- VENDOR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vendor")
19
- if VENDOR not in sys.path:
20
- sys.path.insert(0, VENDOR)
21
-
22
- REPO = "dipta007/OracleZoom"
23
- SD3 = "stabilityai/stable-diffusion-3-medium-diffusers"
24
- VLM = "Qwen/Qwen2.5-VL-3B-Instruct"
25
- COZ_PROMPT = (
26
- "The second image is a zoom-in of the first image. Based on this knowledge, "
27
- "what is in the second image? Give me a set of words."
28
- )
29
- _to_tensor = transforms.Compose([transforms.ToTensor()])
30
-
31
-
32
- class _SRArgs:
33
- def __init__(self, coz_ckpt):
34
- self.lora_path = f"{coz_ckpt}/SR_LoRA/model_20001.pkl"
35
- self.vae_path = f"{coz_ckpt}/SR_VAE/vae_encoder_20001.pt"
36
- self.pretrained_model_name_or_path = SD3
37
- self.process_size = PROCESS_SIZE
38
- self.lora_rank = 4
39
- self.merge_and_unload_lora = False
40
- self.mixed_precision = "fp16"
41
-
42
-
43
- class Models:
44
- """Built once at import, read-only afterwards. Concurrent requests only read."""
45
-
46
- def __init__(self, weights=None):
47
- from huggingface_hub import snapshot_download
48
-
49
- w = weights or snapshot_download(repo_id=REPO)
50
- self.vlm, self.proc, self.process_vision_info = _build_vlm(f"{w}/ckpt/VLM_LoRA/checkpoint-10000")
51
- self.sr = _build_sr(f"{w}/ckpt", f"{w}/merged_transformer.safetensors")
52
-
53
-
54
- def _build_vlm(lora_path):
55
- from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
56
- from qwen_vl_utils import process_vision_info
57
- from peft import PeftModel
58
-
59
- # device_map="auto" asks accelerate to read real GPU memory, which does not exist at
60
- # module scope on ZeroGPU. Place it ourselves. sdpa because flash-attn has no wheel here.
61
- model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
62
- VLM, torch_dtype=torch.bfloat16, attn_implementation="sdpa"
63
- )
64
- # torch_device="cpu" is load-bearing. Left to itself peft calls infer_device(), which reads
65
- # the cuda flag ZeroGPU patches to True, then hands "cuda" to safetensors. That builds
66
- # tensors straight on a device that is not attached yet: RuntimeError, no CUDA GPUs.
67
- model = PeftModel.from_pretrained(model, lora_path, torch_device="cpu").merge_and_unload()
68
- return model.eval().to("cuda"), AutoProcessor.from_pretrained(VLM), process_vision_info
69
-
70
-
71
- def _build_sr(coz_ckpt, merged_transformer):
72
- from safetensors.torch import load_file
73
- from osediff_sd3 import OSEDiff_SD3_TEST, SD3Euler
74
-
75
- # Assemble entirely on CPU, then move once. Two places here materialise weights straight
76
- # onto the model's own device (inject_lora's torch.load map_location, and safetensors), and
77
- # on ZeroGPU that device reads as cuda while no GPU is attached yet.
78
- sr = SD3Euler(device="cpu")
79
- # Construct first, load second. OSEDiff_SD3_TEST swaps every targeted Linear for a
80
- # LoraInjectedLinear, which renames the keys. Our merged checkpoint was saved after that
81
- # swap, so loading it earlier matches nothing and silently leaves the base transformer.
82
- test = OSEDiff_SD3_TEST(_SRArgs(coz_ckpt), sr)
83
- sd = load_file(merged_transformer)
84
- missing, unexpected = test.model.transformer.load_state_dict(
85
- {k: v.to(torch.float32) for k, v in sd.items()}, strict=False)
86
- if len(unexpected) > len(sd) // 2:
87
- raise RuntimeError(f"merged transformer did not match: {len(unexpected)} of {len(sd)} "
88
- f"keys unexpected. LoRA injection order or checkpoint is wrong.")
89
- print(f"#### merged transformer loaded (missing {len(missing)} unexpected {len(unexpected)})")
90
- del sd
91
- gc.collect()
92
-
93
- for m in (sr.text_enc_1, sr.text_enc_2, sr.text_enc_3):
94
- m.to("cuda")
95
- sr.transformer.to("cuda", dtype=torch.float32)
96
- sr.vae.to("cuda", dtype=torch.float32)
97
- for m in (sr.text_enc_1, sr.text_enc_2, sr.text_enc_3, sr.transformer, sr.vae):
98
- m.requires_grad_(False)
99
- sr.device = "cuda" # encode_prompt and set_timesteps read this, not the module devices
100
- return test
101
-
102
-
103
- def write_prompt(models, first, second, max_new_tokens=32):
104
- messages = [
105
- {"role": "system", "content": COZ_PROMPT},
106
- {"role": "user", "content": [{"type": "image", "image": first},
107
- {"type": "image", "image": second}]},
108
- ]
109
- text = models.proc.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
110
- images, videos = models.process_vision_info(messages)
111
- inputs = models.proc(text=[text], images=images, videos=videos,
112
- padding=True, return_tensors="pt").to("cuda")
113
- gen = models.vlm.generate(**inputs, max_new_tokens=max_new_tokens)
114
- trimmed = [o[len(i):] for i, o in zip(inputs.input_ids, gen)]
115
- return models.proc.batch_decode(trimmed, skip_special_tokens=True,
116
- clean_up_tokenization_spaces=False)[0].strip()
117
-
118
-
119
- def super_resolve(models, img, prompt):
120
- lq = _to_tensor(img).unsqueeze(0).to("cuda") * 2 - 1
121
- with torch.no_grad():
122
- out = torch.clamp(models.sr(lq, prompt=prompt)[0].cpu(), -1.0, 1.0)
123
- return transforms.ToPILImage()(out * 0.5 + 0.5)
124
-
125
-
126
- def zoom(models, image, levels=4, upscale=4, center=(0.5, 0.5)):
127
- """Yield one (level, factor, prompt, blurry_input, result) per recursion.
128
-
129
- `blurry_input` is the plain bicubic enlargement the model starts from. It is what the
130
- super-resolution has to improve on, so it doubles as the honest before-picture.
131
- """
132
- cur = resize_and_center_crop(image)
133
- yield 0, 1, "", cur, cur
134
- for i in range(levels):
135
- blurry = zoom_window(cur, upscale, center).resize(cur.size, Image.BICUBIC)
136
- prompt = write_prompt(models, cur, blurry)
137
- cur = super_resolve(models, blurry, prompt)
138
- yield i + 1, upscale ** (i + 1), prompt, blurry, cur