dikdimon commited on
Commit
1933d1d
·
verified ·
1 Parent(s): e2f163a

Upload sd-webui-ultimate-tiled-vae using SD-Hub

Browse files
Files changed (24) hide show
  1. sd-webui-ultimate-tiled-vae/LICENSE +16 -0
  2. sd-webui-ultimate-tiled-vae/README.md +70 -0
  3. sd-webui-ultimate-tiled-vae/scripts/__pycache__/ultimate_tiled_vae.cpython-310.pyc +0 -0
  4. sd-webui-ultimate-tiled-vae/scripts/ultimate_tiled_vae.py +263 -0
  5. sd-webui-ultimate-tiled-vae/tiled_vae/__init__.py +3 -0
  6. sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/__init__.cpython-310.pyc +0 -0
  7. sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/compat.cpython-310.pyc +0 -0
  8. sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/config.cpython-310.pyc +0 -0
  9. sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/executor.cpython-310.pyc +0 -0
  10. sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/fallback.cpython-310.pyc +0 -0
  11. sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/groupnorm.cpython-310.pyc +0 -0
  12. sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/hook.cpython-310.pyc +0 -0
  13. sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/logging_utils.cpython-310.pyc +0 -0
  14. sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/taskqueue.cpython-310.pyc +0 -0
  15. sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/tiling.cpython-310.pyc +0 -0
  16. sd-webui-ultimate-tiled-vae/tiled_vae/compat.py +169 -0
  17. sd-webui-ultimate-tiled-vae/tiled_vae/config.py +160 -0
  18. sd-webui-ultimate-tiled-vae/tiled_vae/executor.py +251 -0
  19. sd-webui-ultimate-tiled-vae/tiled_vae/fallback.py +127 -0
  20. sd-webui-ultimate-tiled-vae/tiled_vae/groupnorm.py +164 -0
  21. sd-webui-ultimate-tiled-vae/tiled_vae/hook.py +256 -0
  22. sd-webui-ultimate-tiled-vae/tiled_vae/logging_utils.py +70 -0
  23. sd-webui-ultimate-tiled-vae/tiled_vae/taskqueue.py +105 -0
  24. sd-webui-ultimate-tiled-vae/tiled_vae/tiling.py +148 -0
sd-webui-ultimate-tiled-vae/LICENSE ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
2
+
3
+ This work is based on "Tiled VAE" by LI YI @ Nanyang Technological University.
4
+ Original source: https://github.com/pkuliyi2015/multidiffusion-upscaler-for-automatic1111
5
+
6
+ You are free to:
7
+ Share — copy and redistribute the material in any medium or format
8
+ Adapt — remix, transform, and build upon the material
9
+
10
+ Under the following terms:
11
+ Attribution — You must give appropriate credit.
12
+ NonCommercial — You may not use the material for commercial purposes.
13
+ ShareAlike — If you remix, transform, or build upon the material,
14
+ you must distribute your contributions under the same license.
15
+
16
+ Full license text: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
sd-webui-ultimate-tiled-vae/README.md ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # sd-webui-ultimate-tiled-vae v1.1
2
+
3
+ Standalone **Tiled VAE** extension for [AUTOMATIC1111 Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui).
4
+
5
+ Splits VAE encode/decode into overlapping tiles so giant images (4K, 8K, panoramas) process without OOM, producing **seamless, artefact-free output**.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # Option A – git (recommended)
11
+ cd /content/A1111/extensions
12
+ git clone https://github.com/yourname/sd-webui-ultimate-tiled-vae
13
+
14
+ # Option B – manual (Colab / no git)
15
+ mkdir -p /content/A1111/extensions/sd-webui-ultimate-tiled-vae
16
+ # unzip or copy all files into that folder
17
+ ```
18
+
19
+ Restart the webui after installation.
20
+
21
+ ## Quick smoke test
22
+
23
+ | Test | Expected |
24
+ |------|----------|
25
+ | txt2img 512×512, extension disabled | Normal generation, no change |
26
+ | txt2img 1024×1024, Fast Approx preset | Tiled execution logged, seamless output |
27
+ | txt2img 2048×2048, Exact Quality preset | Slower, higher quality, no seams |
28
+ | img2img upscale 2× | Works cleanly, hook removed in postprocess |
29
+ | hires.fix on/off | No stuck state after generation |
30
+ | Interrupt mid-decode | Hook still removed on next run |
31
+
32
+ ## Features
33
+
34
+ | Feature | Detail |
35
+ |---------|--------|
36
+ | **3 GroupNorm modes** | *Exact* (full cross-tile sync), *Approx* (from downsampled estimate), *None* (independent) |
37
+ | **GN/fast-mode validation** | Config automatically enforces coherent combinations |
38
+ | **Configurable pad sizes** | Separate encoder/decoder overlap padding |
39
+ | **Smart ignore** | Skips tiling when image fits in one tile |
40
+ | **Auto-adjust tile** | Last tile never larger than first (prevents late OOM) |
41
+ | **NaN-only fp32 retry** | Retries in fp32 only when output actually contains NaN |
42
+ | **Fallback policy** | Abort / Cheap Approx / Original Forward |
43
+ | **Quick Presets** | Exact Quality / Fast Approx / Safe Minimal VRAM |
44
+ | **Standalone attention** | No dependency on `tile_utils.attn` from multidiffusion |
45
+ | **Dynamic scale factor** | Reads from VAE, not hardcoded to 8 |
46
+ | **Safe hook lifecycle** | `try/finally` everywhere; hooks always restored after crash/interrupt |
47
+
48
+ ## GroupNorm Sync modes
49
+
50
+ | Mode | How | When to use |
51
+ |------|-----|-------------|
52
+ | **Exact** | Collect stats from all tiles, weighted average, apply globally | Maximum quality; disables fast mode automatically |
53
+ | **Approx** | Estimate stats on downsampled image; apply to all tiles | Best balance; requires (and auto-enables) fast mode |
54
+ | **None** | Each tile normalises itself | Fastest; may produce faint seams on large uniform areas |
55
+
56
+ ## Roadmap
57
+
58
+ - **Phase 6** – per-block skip matrix (Experimental panel) wired into executor
59
+ - **Phase 7** – SDXL / TAESD explicit guardrails
60
+ - **Phase 8** – Extras-tab postprocess bridge
61
+
62
+ ## Credits
63
+
64
+ - Original Tiled VAE engine: [LI YI / pkuliyi2015](https://github.com/pkuliyi2015/multidiffusion-upscaler-for-automatic1111) — CC BY-NC-SA 4.0
65
+ - Pad size / GN mode ideas: [Kahsolt / stable-diffusion-webui-vae-tile-infer](https://github.com/Kahsolt/stable-diffusion-webui-vae-tile-infer)
66
+ - Tile planner improvements: inspired by [ComfyUI-TiledVaeLite](https://github.com/tinymindlab/comfyui-tiled-vae-lite)
67
+
68
+ ## License
69
+
70
+ CC BY-NC-SA 4.0 — inherited from original engine.
sd-webui-ultimate-tiled-vae/scripts/__pycache__/ultimate_tiled_vae.cpython-310.pyc ADDED
Binary file (7.73 kB). View file
 
sd-webui-ultimate-tiled-vae/scripts/ultimate_tiled_vae.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ sd-webui-ultimate-tiled-vae — v1.1
3
+ Standalone Tiled VAE extension for AUTOMATIC1111 Stable Diffusion WebUI.
4
+
5
+ Based on the original Tiled VAE by LI YI (CC BY-NC-SA 4.0)
6
+ https://github.com/pkuliyi2015/multidiffusion-upscaler-for-automatic1111
7
+ Improvements from:
8
+ • stable-diffusion-webui-vae-tile-infer (Kahsolt)
9
+ • ComfyUI-TiledVaeLite (tile planner ideas)
10
+ """
11
+
12
+ import sys
13
+ import os
14
+
15
+ _EXT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
16
+ if _EXT_DIR not in sys.path:
17
+ sys.path.insert(0, _EXT_DIR)
18
+
19
+ import gradio as gr
20
+ import modules.scripts as scripts
21
+
22
+ from tiled_vae.config import (
23
+ TiledVAEConfig, GNSyncMode, FallbackPolicy,
24
+ preset_exact, preset_fast_approx, preset_safe_minimal,
25
+ )
26
+ from tiled_vae.hook import attach_hooks, detach_hooks, is_hooked
27
+ from tiled_vae.compat import get_input_accordion, get_optimal_device_name
28
+ from tiled_vae.logging_utils import info, warn
29
+
30
+
31
+ # ── VRAM-based recommended defaults ──────────────────────────────────────────
32
+
33
+ def _rcmd_enc() -> int:
34
+ try:
35
+ import torch, modules.devices as devices
36
+ if torch.cuda.is_available() and devices.device not in ["cpu", devices.cpu]:
37
+ mb = torch.cuda.get_device_properties(devices.device).total_memory // 2**20
38
+ if mb > 16_000: return 3072
39
+ elif mb > 12_000: return 2048
40
+ elif mb > 8_000: return 1536
41
+ else: return 960
42
+ except Exception: pass
43
+ return 512
44
+
45
+
46
+ def _rcmd_dec() -> int:
47
+ try:
48
+ import torch, modules.devices as devices
49
+ if torch.cuda.is_available() and devices.device not in ["cpu", devices.cpu]:
50
+ mb = torch.cuda.get_device_properties(devices.device).total_memory // 2**20
51
+ if mb > 30_000: return 256
52
+ elif mb > 16_000: return 192
53
+ elif mb > 12_000: return 128
54
+ elif mb > 8_000: return 96
55
+ else: return 64
56
+ except Exception: pass
57
+ return 64
58
+
59
+
60
+ def _uid(tab: str, name: str) -> str:
61
+ return f"UTV-{tab}-{name}"
62
+
63
+
64
+ # ── Script ────────────────────────────────────────────────────────────────────
65
+
66
+ class Script(scripts.Script):
67
+
68
+ def __init__(self):
69
+ self._hooked = False
70
+
71
+ def title(self):
72
+ return "Ultimate Tiled VAE"
73
+
74
+ def show(self, is_img2img):
75
+ return scripts.AlwaysVisible
76
+
77
+ # ── UI ────────────────────────────────────────────────────────────────────
78
+
79
+ def ui(self, is_img2img):
80
+ tab = "i2i" if is_img2img else "t2i"
81
+ uid = lambda n: _uid(tab, n)
82
+
83
+ InputAccordion = get_input_accordion()
84
+
85
+ if InputAccordion:
86
+ acc = InputAccordion(False, label="Ultimate Tiled VAE", elem_id=uid("acc"))
87
+ else:
88
+ acc = gr.Accordion("Ultimate Tiled VAE", open=False, elem_id=uid("acc"))
89
+
90
+ with acc as enabled_acc:
91
+
92
+ if not InputAccordion:
93
+ enabled = gr.Checkbox(label="Enable Ultimate Tiled VAE", value=False, elem_id=uid("en"))
94
+ else:
95
+ enabled = enabled_acc
96
+
97
+ # Row: GPU + preset
98
+ with gr.Row():
99
+ move_vae_to_gpu = gr.Checkbox(label="Move VAE to GPU", value=True, elem_id=uid("gpu"))
100
+ preset = gr.Dropdown(
101
+ label="Quick Preset",
102
+ choices=["Custom", "Exact Quality", "Fast Approx", "Safe Minimal VRAM"],
103
+ value="Custom",
104
+ elem_id=uid("preset"),
105
+ )
106
+
107
+ # Tile sizes
108
+ gr.HTML('<p style="margin:0.4em 0 0.2em"><b>Tile Sizes</b> — larger = better quality, more VRAM.</p>')
109
+ with gr.Row():
110
+ encoder_tile_size = gr.Slider(label="Encoder Tile Size", minimum=256, maximum=4096, step=16, value=_rcmd_enc(), elem_id=uid("enc_tile"))
111
+ decoder_tile_size = gr.Slider(label="Decoder Tile Size", minimum=48, maximum=512, step=16, value=_rcmd_dec(), elem_id=uid("dec_tile"))
112
+ gr.Button("↻ Reset", variant="tool", elem_id=uid("reset")).click(
113
+ fn=lambda: [_rcmd_enc(), _rcmd_dec()],
114
+ outputs=[encoder_tile_size, decoder_tile_size],
115
+ show_progress=False,
116
+ )
117
+
118
+ # Pad sizes
119
+ with gr.Row():
120
+ encoder_pad_size = gr.Slider(label="Encoder Pad", minimum=0, maximum=64, step=1, value=32, elem_id=uid("enc_pad"))
121
+ decoder_pad_size = gr.Slider(label="Decoder Pad", minimum=0, maximum=32, step=1, value=11, elem_id=uid("dec_pad"))
122
+
123
+ # Fast mode + color fix
124
+ with gr.Row():
125
+ fast_encoder = gr.Checkbox(label="Fast Encoder", value=True, elem_id=uid("fenc"))
126
+ fast_decoder = gr.Checkbox(label="Fast Decoder", value=True, elem_id=uid("fdec"))
127
+ color_fix = gr.Checkbox(label="Encoder Color Fix", value=False, elem_id=uid("cfix"))
128
+ fast_encoder.change(fn=lambda v: gr.update(visible=v), inputs=fast_encoder, outputs=color_fix, show_progress=False)
129
+
130
+ # GN sync
131
+ gn_sync_mode = gr.Radio(
132
+ label="GroupNorm Sync (Approx auto-enables fast mode; Exact auto-disables it)",
133
+ choices=[m.value for m in GNSyncMode],
134
+ value=GNSyncMode.APPROX.value,
135
+ elem_id=uid("gn"),
136
+ )
137
+
138
+ # Auto / smart / log
139
+ with gr.Row():
140
+ auto_adjust = gr.Checkbox(label="Auto Adjust Tile Size", value=True, elem_id=uid("adj"))
141
+ smart_ignore = gr.Checkbox(label="Smart Ignore Small Images", value=True, elem_id=uid("si"))
142
+ log_timings = gr.Checkbox(label="Log Timings", value=True, elem_id=uid("log"))
143
+
144
+ # Reliability
145
+ with gr.Row():
146
+ retry_fp32 = gr.Checkbox(label="Retry in FP32 on NaN", value=True, elem_id=uid("fp32"))
147
+ fallback_policy = gr.Dropdown(
148
+ label="Fallback on failure",
149
+ choices=[p.value for p in FallbackPolicy],
150
+ value=FallbackPolicy.CHEAP_APPROX.value,
151
+ elem_id=uid("fb"),
152
+ )
153
+
154
+ # Experimental (Phase 6: per-block skip matrix not yet implemented)
155
+ with gr.Accordion("Experimental", open=False, elem_id=uid("exp")):
156
+ gr.HTML(
157
+ "<p style='color:orange;margin:0'>"
158
+ "⚠ Experimental options may produce artefacts. "
159
+ "Per-block skip matrix arrives in Phase 6.</p>"
160
+ )
161
+ skip_infer = gr.Checkbox(
162
+ label="Enable Skip Infer (no per-block control yet)",
163
+ value=False,
164
+ elem_id=uid("skip"),
165
+ )
166
+
167
+ # ── Preset wiring ─────────────────────────────────────────────────
168
+ def _apply_preset(name):
169
+ enc, dec = _rcmd_enc(), _rcmd_dec()
170
+ if name == "Exact Quality": c = preset_exact(enc, dec)
171
+ elif name == "Fast Approx": c = preset_fast_approx(enc, dec)
172
+ elif name == "Safe Minimal VRAM": c = preset_safe_minimal(enc, dec)
173
+ else:
174
+ return (gr.update(),) * 7
175
+ return (
176
+ c.fast_encoder, c.fast_decoder, c.encoder_color_fix,
177
+ c.gn_sync_mode, c.auto_adjust_tile, c.smart_ignore,
178
+ c.fallback_policy,
179
+ )
180
+
181
+ preset.change(
182
+ fn=_apply_preset,
183
+ inputs=[preset],
184
+ outputs=[fast_encoder, fast_decoder, color_fix, gn_sync_mode,
185
+ auto_adjust, smart_ignore, fallback_policy],
186
+ show_progress=False,
187
+ )
188
+
189
+ return [
190
+ enabled, move_vae_to_gpu,
191
+ encoder_tile_size, decoder_tile_size,
192
+ encoder_pad_size, decoder_pad_size,
193
+ fast_encoder, fast_decoder, color_fix,
194
+ gn_sync_mode,
195
+ auto_adjust, smart_ignore,
196
+ retry_fp32, fallback_policy,
197
+ log_timings, skip_infer,
198
+ ]
199
+
200
+ # ── process ───────────────────────────────────────────────────────────────
201
+
202
+ def process(self, p, *args):
203
+ (
204
+ enabled, move_vae_to_gpu,
205
+ encoder_tile_size, decoder_tile_size,
206
+ encoder_pad_size, decoder_pad_size,
207
+ fast_encoder, fast_decoder, color_fix,
208
+ gn_sync_mode,
209
+ auto_adjust, smart_ignore,
210
+ retry_fp32, fallback_policy,
211
+ log_timings, skip_infer,
212
+ ) = args
213
+
214
+ vae = p.sd_model.first_stage_model
215
+
216
+ if not enabled:
217
+ if is_hooked(vae):
218
+ detach_hooks(vae)
219
+ self._hooked = False
220
+ return
221
+
222
+ # VRAM hint
223
+ try:
224
+ import modules.devices as devices
225
+ if get_optimal_device_name().startswith("cuda") \
226
+ and vae.device == devices.cpu and not move_vae_to_gpu:
227
+ warn("VAE is on CPU. Enable 'Move VAE to GPU' for better performance.")
228
+ except Exception:
229
+ pass
230
+
231
+ cfg = TiledVAEConfig(
232
+ enabled=True,
233
+ move_vae_to_gpu=move_vae_to_gpu,
234
+ encoder_tile_size=int(encoder_tile_size),
235
+ decoder_tile_size=int(decoder_tile_size),
236
+ encoder_pad_size=int(encoder_pad_size),
237
+ decoder_pad_size=int(decoder_pad_size),
238
+ fast_encoder=fast_encoder,
239
+ fast_decoder=fast_decoder,
240
+ encoder_color_fix=color_fix,
241
+ gn_sync_mode=gn_sync_mode,
242
+ auto_adjust_tile=auto_adjust,
243
+ smart_ignore=smart_ignore,
244
+ retry_fp32_on_nan=retry_fp32,
245
+ fallback_policy=fallback_policy,
246
+ log_timings=log_timings,
247
+ skip_infer=skip_infer,
248
+ ).validate()
249
+
250
+ attach_hooks(vae, cfg)
251
+ self._hooked = True
252
+
253
+ # ── postprocess: always clean up ─────────────────────────────────────────
254
+
255
+ def postprocess(self, p, processed, *args):
256
+ try:
257
+ vae = p.sd_model.first_stage_model
258
+ if is_hooked(vae):
259
+ detach_hooks(vae)
260
+ except Exception as e:
261
+ warn(f"postprocess cleanup failed: {e}")
262
+ finally:
263
+ self._hooked = False
sd-webui-ultimate-tiled-vae/tiled_vae/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """Ultimate Tiled VAE – internal package."""
2
+ from .config import TiledVAEConfig, GNSyncMode, FallbackPolicy
3
+ from .hook import attach_hooks, detach_hooks, is_hooked, VAEHook
sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (421 Bytes). View file
 
sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/compat.cpython-310.pyc ADDED
Binary file (5.4 kB). View file
 
sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/config.cpython-310.pyc ADDED
Binary file (4.25 kB). View file
 
sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/executor.cpython-310.pyc ADDED
Binary file (5.34 kB). View file
 
sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/fallback.cpython-310.pyc ADDED
Binary file (4.18 kB). View file
 
sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/groupnorm.cpython-310.pyc ADDED
Binary file (5.87 kB). View file
 
sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/hook.cpython-310.pyc ADDED
Binary file (7 kB). View file
 
sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/logging_utils.cpython-310.pyc ADDED
Binary file (2.16 kB). View file
 
sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/taskqueue.cpython-310.pyc ADDED
Binary file (3.68 kB). View file
 
sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/tiling.cpython-310.pyc ADDED
Binary file (4.44 kB). View file
 
sd-webui-ultimate-tiled-vae/tiled_vae/compat.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Compatibility shims for different A1111 / torch / model variants.
3
+
4
+ IMPORTANT – attention contract:
5
+ _attn2task() in taskqueue.py decomposes attention into:
6
+ store_res -> pre_norm -> attn -> add_res
7
+
8
+ Therefore the `attn` callable receives an ALREADY-NORMALIZED tensor
9
+ and must return ONLY the projected attention output — no residual add,
10
+ no second norm call. The residual is re-added by the `add_res` task.
11
+
12
+ The old fallback violated that contract (called block.norm again and
13
+ returned x + out). Fixed in this version.
14
+ """
15
+
16
+ from __future__ import annotations
17
+ from typing import Any, Callable
18
+
19
+ import torch
20
+
21
+
22
+ # ── A1111 module helpers ───────────────────────────────────────────────────────
23
+
24
+ def get_devices_module():
25
+ import modules.devices as devices
26
+ return devices
27
+
28
+
29
+ def get_optimal_device() -> torch.device:
30
+ return get_devices_module().get_optimal_device()
31
+
32
+
33
+ def get_optimal_device_name() -> str:
34
+ devs = get_devices_module()
35
+ if hasattr(devs, "get_optimal_device_name"):
36
+ return devs.get_optimal_device_name()
37
+ return str(devs.get_optimal_device())
38
+
39
+
40
+ def torch_gc() -> None:
41
+ import gc
42
+ get_devices_module().torch_gc()
43
+ gc.collect()
44
+
45
+
46
+ def test_for_nans(tensor: torch.Tensor, where: str = "vae") -> None:
47
+ get_devices_module().test_for_nans(tensor, where)
48
+
49
+
50
+ # ── A1111 UI helpers ──────────────────────────────────────────────────────────
51
+
52
+ def get_input_accordion():
53
+ try:
54
+ from modules.shared import opts
55
+ if hasattr(opts, "hypertile_enable_unet"):
56
+ from modules.ui_components import InputAccordion
57
+ return InputAccordion
58
+ except Exception:
59
+ pass
60
+ return None
61
+
62
+
63
+ # ── Model inspection ──────────────────────────────────────────────────────────
64
+
65
+ def is_taesd(vae_model: Any) -> bool:
66
+ return "taesd" in type(vae_model).__name__.lower()
67
+
68
+
69
+ def is_sdxl(vae_model: Any) -> bool:
70
+ try:
71
+ enc = vae_model.encoder
72
+ if hasattr(enc, "z_channels"):
73
+ return enc.z_channels == 16
74
+ if hasattr(enc, "conv_out"):
75
+ return enc.conv_out.out_channels == 32
76
+ except Exception:
77
+ pass
78
+ return False
79
+
80
+
81
+ def get_vae_scale_factor(vae_model: Any) -> int:
82
+ """Return the spatial scale factor (encoder /scale, decoder *scale). Usually 8."""
83
+ try:
84
+ return int(vae_model.downscale_factor)
85
+ except Exception:
86
+ pass
87
+ try:
88
+ from modules.processing import opt_f
89
+ return int(opt_f)
90
+ except Exception:
91
+ return 8
92
+
93
+
94
+ # ── Attention function factory ────────────────────────────────────────────────
95
+ #
96
+ # CONTRACT (must be honoured by every path):
97
+ # f(block, x) where x is ALREADY GroupNorm-normalised.
98
+ # Returns ONLY proj_out(attn(x)).
99
+ # Must NOT call block.norm(x).
100
+ # Must NOT add the residual (the add_res task does that).
101
+ #
102
+ # This module is fully self-contained; tile_utils.attn from multidiffusion
103
+ # is no longer a dependency.
104
+
105
+ def _vanilla_attn_output(block: Any, x: torch.Tensor) -> torch.Tensor:
106
+ """Scaled dot-product attention on an already-normalised x. No norm, no residual."""
107
+ B, C, H, W = x.shape
108
+ q = block.q(x).reshape(B, C, H * W).permute(0, 2, 1) # (B, HW, C)
109
+ k = block.k(x).reshape(B, C, H * W) # (B, C, HW)
110
+ v = block.v(x).reshape(B, C, H * W) # (B, C, HW)
111
+
112
+ w = torch.bmm(q, k) * (int(C) ** -0.5) # (B, HW, HW)
113
+ w = torch.softmax(w, dim=2).permute(0, 2, 1)
114
+
115
+ out = torch.bmm(v, w).reshape(B, C, H, W)
116
+ return block.proj_out(out)
117
+
118
+
119
+ def _xformers_attn_output(block: Any, x: torch.Tensor) -> torch.Tensor:
120
+ """
121
+ xformers memory-efficient attention on an already-normalised x.
122
+ No norm, no residual.
123
+
124
+ Full axis layout (both directions must be correct and symmetric):
125
+
126
+ INPUT: block.q/k/v return (B, C, H, W)
127
+ xformers expects (B, seq, C)
128
+ Convert: (B, C, H, W) -> permute(0,2,3,1) -> (B, H, W, C)
129
+ -> reshape -> (B, H*W, C)
130
+ A plain .reshape(B, H*W, C) from (B, C, H, W) silently
131
+ interleaves channel and spatial bytes — wrong.
132
+
133
+ OUTPUT: xformers returns (B, H*W, C)
134
+ We need (B, C, H, W)
135
+ Convert: (B, H*W, C) -> reshape -> (B, H, W, C)
136
+ -> permute(0,3,1,2) -> (B, C, H, W)
137
+ A plain .reshape(B, C, H, W) from (B, HW, C) is equally
138
+ wrong for the same reason.
139
+ """
140
+ try:
141
+ import xformers.ops as xops
142
+ B, C, H, W = x.shape
143
+ # input: (B, C, H, W) → (B, HW, C) [permute THEN reshape]
144
+ q = block.q(x).permute(0, 2, 3, 1).reshape(B, H * W, C)
145
+ k = block.k(x).permute(0, 2, 3, 1).reshape(B, H * W, C)
146
+ v = block.v(x).permute(0, 2, 3, 1).reshape(B, H * W, C)
147
+ # out: (B, HW, C)
148
+ out = xops.memory_efficient_attention(q, k, v)
149
+ # output: (B, HW, C) → (B, C, H, W) [reshape THEN permute]
150
+ out = out.reshape(B, H, W, C).permute(0, 3, 1, 2).contiguous()
151
+ return block.proj_out(out)
152
+ except Exception:
153
+ # xformers unavailable or call failed – fall back to vanilla
154
+ return _vanilla_attn_output(block, x)
155
+
156
+
157
+ def get_attn_func(block: Any) -> Callable:
158
+ """
159
+ Return the correct attention callable for `block`.
160
+ All paths honour the contract: pre-normed input, proj_out output only.
161
+ No tile_utils dependency.
162
+ """
163
+ try:
164
+ from ldm.modules.diffusionmodules.model import MemoryEfficientAttnBlock
165
+ if isinstance(block, MemoryEfficientAttnBlock):
166
+ return _xformers_attn_output
167
+ except ImportError:
168
+ pass
169
+ return _vanilla_attn_output
sd-webui-ultimate-tiled-vae/tiled_vae/config.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TiledVAEConfig – dataclass + validation + presets.
3
+
4
+ GN mode / fast-mode contract (enforced in validate()):
5
+ Approx -> requires fast estimation -> forces fast_encoder/fast_decoder True
6
+ Exact -> requires exact path -> forces fast_encoder/fast_decoder False
7
+ None -> each tile is independent -> fast_* has no effect on GN, allowed either way
8
+ """
9
+
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+
13
+
14
+ class GNSyncMode(str, Enum):
15
+ EXACT = "Exact" # collect stats from all tiles, weighted average
16
+ APPROX = "Approx" # stats from downsampled full-image estimate
17
+ NONE = "None" # each tile normalises itself (fastest, may seam)
18
+
19
+
20
+ class FallbackPolicy(str, Enum):
21
+ ABORT = "Abort"
22
+ CHEAP_APPROX = "Cheap Approx"
23
+ ORIGINAL = "Original Forward"
24
+
25
+
26
+ @dataclass
27
+ class TiledVAEConfig:
28
+ # ── Core toggles ──────────────────────────────────────────────────────────
29
+ enabled: bool = False
30
+ move_vae_to_gpu: bool = True
31
+
32
+ # ── Tile geometry ─────────────────────────────────────────────────────────
33
+ encoder_tile_size: int = 960
34
+ decoder_tile_size: int = 64
35
+ encoder_pad_size: int = 32
36
+ decoder_pad_size: int = 11
37
+
38
+ # ── Quality / speed knobs ─────────────────────────────────────────────────
39
+ fast_encoder: bool = True
40
+ fast_decoder: bool = True
41
+ encoder_color_fix: bool = False
42
+ gn_sync_mode: str = GNSyncMode.APPROX.value
43
+
44
+ # ── Auto-sizing ───────────────────────────────────────────────────────────
45
+ auto_adjust_tile: bool = True
46
+ smart_ignore: bool = True
47
+
48
+ # ── Reliability ───────────────────────────────────────────────────────────
49
+ retry_fp32_on_nan: bool = True
50
+ fallback_policy: str = FallbackPolicy.CHEAP_APPROX.value
51
+
52
+ # ── Logging ───────────────────────────────────────────────────────────────
53
+ log_timings: bool = True
54
+
55
+ # ── Experimental ─────────────────────────────────────────────────────────
56
+ skip_infer: bool = False
57
+ # Per-block skip dicts — populated in Phase 6
58
+ skip_infer_enc: dict = field(default_factory=dict)
59
+ skip_infer_dec: dict = field(default_factory=dict)
60
+
61
+ # ─────────────────────────────────────────────────────────────────────────
62
+ def validate(self) -> "TiledVAEConfig":
63
+ # Clamp numeric fields
64
+ self.encoder_tile_size = max(256, int(self.encoder_tile_size))
65
+ self.decoder_tile_size = max(48, int(self.decoder_tile_size))
66
+ self.encoder_pad_size = max(0, int(self.encoder_pad_size))
67
+ self.decoder_pad_size = max(0, int(self.decoder_pad_size))
68
+
69
+ # Validate enum strings
70
+ valid_gn = {m.value for m in GNSyncMode}
71
+ if self.gn_sync_mode not in valid_gn:
72
+ self.gn_sync_mode = GNSyncMode.APPROX.value
73
+
74
+ valid_fb = {p.value for p in FallbackPolicy}
75
+ if self.fallback_policy not in valid_fb:
76
+ self.fallback_policy = FallbackPolicy.CHEAP_APPROX.value
77
+
78
+ # ── GN mode / fast-mode coherence ────────────────────────────────────
79
+ # Approx mode is implemented by running estimate_group_norm on a
80
+ # downsampled image BEFORE tile execution. That path is only activated
81
+ # when fast_encoder / fast_decoder is True. If the user picks Approx
82
+ # but disables fast mode, the executor would silently fall through to
83
+ # the layer's own forward (effectively None). We prevent that by
84
+ # auto-enabling fast mode.
85
+ if self.gn_sync_mode == GNSyncMode.APPROX.value:
86
+ if not self.fast_encoder:
87
+ self.fast_encoder = True
88
+ _warn_coerce("GN=Approx requires fast encoder — enabling fast_encoder.")
89
+ if not self.fast_decoder:
90
+ self.fast_decoder = True
91
+ _warn_coerce("GN=Approx requires fast decoder — enabling fast_decoder.")
92
+
93
+ # Exact mode relies on the EXACT task-queue path (no pre-estimation).
94
+ # If fast mode is on, estimate_group_norm replaces pre_norm tasks with
95
+ # apply_norm and the exact cross-tile accumulation never happens.
96
+ # Force fast_* off so the semantics are correct.
97
+ if self.gn_sync_mode == GNSyncMode.EXACT.value:
98
+ if self.fast_encoder:
99
+ self.fast_encoder = False
100
+ _warn_coerce("GN=Exact requires fast_encoder=False — disabling.")
101
+ if self.fast_decoder:
102
+ self.fast_decoder = False
103
+ _warn_coerce("GN=Exact requires fast_decoder=False — disabling.")
104
+ # color_fix is also a fast-encoder sub-feature
105
+ self.encoder_color_fix = False
106
+
107
+ return self
108
+
109
+
110
+ def _warn_coerce(msg: str) -> None:
111
+ print(f"[Ultimate Tiled VAE] config: {msg}")
112
+
113
+
114
+ # ── Preset factory ────────────────────────────────────────────────────────────
115
+
116
+ def preset_exact(enc_tile: int, dec_tile: int) -> TiledVAEConfig:
117
+ """Maximum quality – full GN sync, no fast paths (auto-validated)."""
118
+ return TiledVAEConfig(
119
+ enabled=True,
120
+ encoder_tile_size=enc_tile,
121
+ decoder_tile_size=dec_tile,
122
+ fast_encoder=False,
123
+ fast_decoder=False,
124
+ encoder_color_fix=False,
125
+ gn_sync_mode=GNSyncMode.EXACT.value,
126
+ retry_fp32_on_nan=True,
127
+ fallback_policy=FallbackPolicy.CHEAP_APPROX.value,
128
+ ).validate()
129
+
130
+
131
+ def preset_fast_approx(enc_tile: int, dec_tile: int) -> TiledVAEConfig:
132
+ """Good balance of quality and speed (auto-validated)."""
133
+ return TiledVAEConfig(
134
+ enabled=True,
135
+ encoder_tile_size=enc_tile,
136
+ decoder_tile_size=dec_tile,
137
+ fast_encoder=True,
138
+ fast_decoder=True,
139
+ encoder_color_fix=True,
140
+ gn_sync_mode=GNSyncMode.APPROX.value,
141
+ move_vae_to_gpu=True,
142
+ retry_fp32_on_nan=True,
143
+ fallback_policy=FallbackPolicy.CHEAP_APPROX.value,
144
+ ).validate()
145
+
146
+
147
+ def preset_safe_minimal(enc_tile: int, dec_tile: int) -> TiledVAEConfig:
148
+ """Minimal VRAM – independent tile norm, immediate fp32 retry."""
149
+ return TiledVAEConfig(
150
+ enabled=True,
151
+ encoder_tile_size=enc_tile,
152
+ decoder_tile_size=max(48, dec_tile // 2),
153
+ fast_encoder=True,
154
+ fast_decoder=True,
155
+ encoder_color_fix=False,
156
+ gn_sync_mode=GNSyncMode.NONE.value,
157
+ smart_ignore=True,
158
+ retry_fp32_on_nan=True,
159
+ fallback_policy=FallbackPolicy.ORIGINAL.value,
160
+ ).validate()
sd-webui-ultimate-tiled-vae/tiled_vae/executor.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tile execution engine for Ultimate Tiled VAE.
3
+
4
+ Zigzag order minimises GPU<->CPU transfers.
5
+ Supports all three GN sync modes.
6
+ scale parameter is threaded through so no hardcoded 8.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ from typing import List, Optional, TYPE_CHECKING
11
+
12
+ import torch
13
+ import torch.nn.functional as F
14
+ from tqdm import tqdm
15
+
16
+ from .config import GNSyncMode
17
+ from .groupnorm import GroupNormParam
18
+ from .tiling import crop_valid_region
19
+ from .logging_utils import info
20
+
21
+ if TYPE_CHECKING:
22
+ from .config import TiledVAEConfig
23
+
24
+
25
+ @torch.no_grad()
26
+ def estimate_group_norm(
27
+ z: torch.Tensor,
28
+ task_queue: List,
29
+ color_fix: bool,
30
+ ) -> bool:
31
+ """
32
+ Fast-mode: run forward on downsampled z to collect GN parameters.
33
+ Replaces 'pre_norm' tasks in-place with 'apply_norm'.
34
+ Returns True on success, False on NaN (caller falls back to exact mode).
35
+ """
36
+ tile = z
37
+ last_id = len(task_queue) - 1
38
+ while last_id >= 0 and task_queue[last_id][0] != "pre_norm":
39
+ last_id -= 1
40
+ if last_id <= 0:
41
+ raise ValueError("No GroupNorm found in task queue")
42
+
43
+ for i in range(last_id + 1):
44
+ task = task_queue[i]
45
+ name = task[0]
46
+
47
+ if name == "pre_norm":
48
+ gn_func = GroupNormParam.from_tile(tile, task[1])
49
+ task_queue[i] = ["apply_norm", gn_func]
50
+ if i == last_id:
51
+ return True
52
+ tile = gn_func(tile)
53
+
54
+ elif name in ("store_res", "store_res_cpu"):
55
+ j = i + 1
56
+ while j < last_id and task_queue[j][0] != "add_res":
57
+ j += 1
58
+ if j < last_id:
59
+ res = task[1](tile)
60
+ task_queue[j][1] = res.cpu() if name == "store_res_cpu" else res
61
+
62
+ elif name == "add_res":
63
+ tile = tile + task[1].to(tile.device)
64
+ task[1] = None
65
+
66
+ elif color_fix and name == "downsample":
67
+ # encoder color fix: estimate only up to first downsample
68
+ for k in range(i, last_id + 1):
69
+ if task_queue[k][0] == "store_res":
70
+ task_queue[k][0] = "store_res_cpu"
71
+ return True
72
+
73
+ else:
74
+ tile = task[1](tile)
75
+
76
+ # NaN guard during estimation
77
+ if torch.isnan(tile).any():
78
+ info("NaN during fast-mode estimation — disabling fast mode.")
79
+ return False
80
+
81
+ raise IndexError("estimate_group_norm: should not reach here")
82
+
83
+
84
+ def _prepare_fast_mode(
85
+ z: torch.Tensor,
86
+ tile_size: int,
87
+ task_queue_template: List,
88
+ color_fix: bool,
89
+ device: torch.device,
90
+ ) -> List:
91
+ """
92
+ Downsample z, fix distribution shift, run estimate_group_norm.
93
+ Returns updated queue on success, original template on failure.
94
+ """
95
+ H, W = z.shape[2], z.shape[3]
96
+ scale_factor = tile_size / max(H, W)
97
+ z_dev = z.to(device)
98
+ z_down = F.interpolate(z_dev, scale_factor=scale_factor, mode="nearest-exact")
99
+
100
+ # Restore distribution (downsampling shifts mean/std)
101
+ std_old, mean_old = torch.std_mean(z_dev, dim=[0, 2, 3], keepdim=True)
102
+ std_new, mean_new = torch.std_mean(z_down, dim=[0, 2, 3], keepdim=True)
103
+ z_down = (z_down - mean_new) / std_new * std_old + mean_old
104
+ del std_old, mean_old, std_new, mean_new
105
+ z_down = torch.clamp_(z_down, min=z_dev.min(), max=z_dev.max())
106
+
107
+ info(
108
+ f"Fast mode: GN estimation on "
109
+ f"{z_down.shape[3]}×{z_down.shape[2]} downsampled image"
110
+ )
111
+
112
+ from .taskqueue import clone_task_queue
113
+ est_queue = clone_task_queue(task_queue_template)
114
+ if estimate_group_norm(z_down, est_queue, color_fix=color_fix):
115
+ return est_queue
116
+ return task_queue_template # fall back to exact
117
+
118
+
119
+ @torch.no_grad()
120
+ def execute_tiles(
121
+ tiles: List[Optional[torch.Tensor]],
122
+ task_queues: List[List],
123
+ in_bboxes,
124
+ out_bboxes,
125
+ result_shape,
126
+ device: torch.device,
127
+ dtype: torch.dtype,
128
+ is_decoder: bool,
129
+ gn_sync_mode: str,
130
+ cfg: "TiledVAEConfig",
131
+ interrupted_flag,
132
+ scale: int = 8,
133
+ ) -> Optional[torch.Tensor]:
134
+ """
135
+ Execute all task queues in zigzag order.
136
+ Returns assembled result tensor, or None if interrupted.
137
+ """
138
+ num_tiles = len(tiles)
139
+ num_completed = 0
140
+ result: Optional[torch.Tensor] = None
141
+ forward = True
142
+
143
+ total_steps = num_tiles * len(task_queues[0])
144
+ pbar = tqdm(
145
+ total=total_steps,
146
+ desc=f"[Tiled VAE] {'Decoder' if is_decoder else 'Encoder'}",
147
+ )
148
+
149
+ try:
150
+ while True:
151
+ if interrupted_flag.interrupted:
152
+ return None
153
+
154
+ gn_param = GroupNormParam()
155
+ tile_iter = range(num_tiles) if forward else reversed(range(num_tiles))
156
+
157
+ for i in tile_iter:
158
+ if interrupted_flag.interrupted:
159
+ return None
160
+
161
+ tile = tiles[i].to(device)
162
+ task_queue = task_queues[i]
163
+ in_bbox = in_bboxes[i]
164
+
165
+ while task_queue:
166
+ if interrupted_flag.interrupted:
167
+ return None
168
+
169
+ task = task_queue.pop(0)
170
+ name = task[0]
171
+
172
+ if name == "pre_norm":
173
+ if gn_sync_mode == GNSyncMode.EXACT.value:
174
+ # Suspend this tile until all tiles contribute stats
175
+ gn_param.add_tile(tile, task[1])
176
+ break
177
+ elif gn_sync_mode == GNSyncMode.APPROX.value:
178
+ # apply_norm was injected by estimate_group_norm
179
+ # If somehow pre_norm slipped through, use layer forward
180
+ tile = task[1](tile)
181
+ else: # NONE
182
+ tile = task[1](tile)
183
+
184
+ elif name == "apply_norm":
185
+ tile = task[1](tile)
186
+
187
+ elif name in ("store_res", "store_res_cpu"):
188
+ res = task[1](tile)
189
+ if name == "store_res_cpu":
190
+ res = res.cpu()
191
+ j = 0
192
+ while j < len(task_queue) and task_queue[j][0] != "add_res":
193
+ j += 1
194
+ if j < len(task_queue):
195
+ task_queue[j][1] = res
196
+
197
+ elif name == "add_res":
198
+ tile = tile + task[1].to(device)
199
+ task[1] = None
200
+
201
+ else:
202
+ tile = task[1](tile)
203
+
204
+ pbar.update(1)
205
+
206
+ # Write completed tile
207
+ if not task_queue:
208
+ # NaN check on completed tile
209
+ try:
210
+ import modules.devices as devmod
211
+ devmod.test_for_nans(tile, "vae")
212
+ except Exception:
213
+ raise
214
+
215
+ N = result_shape[0]
216
+ if result is None:
217
+ result = torch.zeros(result_shape, device=device, requires_grad=False)
218
+
219
+ valid = crop_valid_region(tile, in_bbox, out_bboxes[i], is_decoder, scale=scale)
220
+ ob = out_bboxes[i]
221
+ result[:, :, ob[2]:ob[3], ob[0]:ob[1]] = valid
222
+ tiles[i] = None
223
+ num_completed += 1
224
+ del tile
225
+
226
+ elif i == num_tiles - 1 and forward:
227
+ forward = False
228
+ tiles[i] = tile
229
+
230
+ elif i == 0 and not forward:
231
+ forward = True
232
+ tiles[i] = tile
233
+
234
+ else:
235
+ tiles[i] = tile.cpu()
236
+ del tile
237
+
238
+ if num_completed == num_tiles:
239
+ break
240
+
241
+ # EXACT mode: inject weighted GN for the next round
242
+ if gn_sync_mode == GNSyncMode.EXACT.value:
243
+ gn_func = gn_param.summary()
244
+ if gn_func is not None:
245
+ for tq in task_queues:
246
+ tq.insert(0, ["apply_norm", gn_func])
247
+
248
+ finally:
249
+ pbar.close()
250
+
251
+ return result
sd-webui-ultimate-tiled-vae/tiled_vae/fallback.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fallback and NaN-retry logic for Ultimate Tiled VAE.
3
+
4
+ retry_fp32_on_nan triggers ONLY when a NaN is actually present in the
5
+ output tensor — not on every exception. Other exceptions propagate normally
6
+ to the caller (hook.py) which then applies the fallback policy.
7
+
8
+ Fallback policy (FallbackPolicy):
9
+ Abort — re-raise; user sees an error message.
10
+ Cheap Approx — return cheap_approximation (decoder only, encoder aborts).
11
+ Original Forward — call the unhooked forward function.
12
+ """
13
+
14
+ from __future__ import annotations
15
+ from typing import Optional, TYPE_CHECKING
16
+
17
+ import torch
18
+ import torch.nn.functional as F
19
+
20
+ from .config import FallbackPolicy
21
+ from .logging_utils import warn, info
22
+
23
+ if TYPE_CHECKING:
24
+ from .config import TiledVAEConfig
25
+
26
+
27
+ def has_nans(tensor: torch.Tensor) -> bool:
28
+ return bool(torch.isnan(tensor).any())
29
+
30
+
31
+ def retry_in_fp32(tiled_forward_fn, x: torch.Tensor, net) -> Optional[torch.Tensor]:
32
+ """
33
+ Re-run the tiled forward pass with the network temporarily cast to fp32.
34
+ Returns the result (cast back to original dtype) on success, or None.
35
+
36
+ Called ONLY when NaN is detected in the result — not on generic exceptions.
37
+ """
38
+ orig_dtype = next(net.parameters()).dtype
39
+ if orig_dtype == torch.float32:
40
+ return None # Already fp32, no point retrying
41
+
42
+ info("NaN detected in output — retrying in fp32 …")
43
+ original_dtype_for_restore = orig_dtype
44
+ try:
45
+ net.float()
46
+ with torch.no_grad():
47
+ result_fp32 = tiled_forward_fn(x.float())
48
+ if has_nans(result_fp32):
49
+ warn("fp32 retry also produced NaN.")
50
+ return None
51
+ return result_fp32.to(original_dtype_for_restore)
52
+ except Exception as e:
53
+ warn(f"fp32 retry raised an exception: {e}")
54
+ return None
55
+ finally:
56
+ try:
57
+ net.to(original_dtype_for_restore)
58
+ except Exception:
59
+ pass
60
+
61
+
62
+ def apply_fallback(
63
+ cfg: "TiledVAEConfig",
64
+ x: torch.Tensor,
65
+ net,
66
+ original_forward,
67
+ result_approx: Optional[torch.Tensor],
68
+ exc: Exception,
69
+ is_decoder: bool,
70
+ ) -> torch.Tensor:
71
+ """
72
+ Apply the configured fallback policy after an unrecoverable tiling error.
73
+ """
74
+ policy = cfg.fallback_policy
75
+
76
+ if policy == FallbackPolicy.ORIGINAL.value:
77
+ warn("Falling back to original (un-tiled) VAE forward …")
78
+ try:
79
+ with torch.no_grad():
80
+ # Ensure input is on the same device as the network
81
+ device = next(net.parameters()).device
82
+ return original_forward(x.to(device))
83
+ except Exception as e2:
84
+ warn(f"Original forward also failed: {e2}")
85
+ raise exc from e2
86
+
87
+ elif policy == FallbackPolicy.CHEAP_APPROX.value:
88
+ if is_decoder and result_approx is not None:
89
+ warn("Falling back to cheap approximation output …")
90
+ try:
91
+ import modules.devices as devices
92
+ dtype = next(net.parameters()).dtype
93
+ return result_approx.to(devices.device, dtype=dtype)
94
+ except Exception as e2:
95
+ warn(f"Cheap approx fallback failed: {e2}")
96
+ # Encoder has no approx, or approx failed → escalate
97
+ raise exc
98
+
99
+ else: # Abort
100
+ raise exc
101
+
102
+
103
+ def get_cheap_approx_result(
104
+ z: torch.Tensor,
105
+ is_decoder: bool,
106
+ ) -> Optional[torch.Tensor]:
107
+ """
108
+ Pre-compute cheap_approximation as a safety net for the decoder.
109
+ Returns None for encoder (no equivalent approximation).
110
+ """
111
+ if not is_decoder:
112
+ return None
113
+ try:
114
+ from modules.sd_vae_approx import cheap_approximation
115
+ from modules.processing import opt_f
116
+ with torch.no_grad():
117
+ return torch.cat(
118
+ [F.interpolate(
119
+ cheap_approximation(xi.unsqueeze(0)),
120
+ scale_factor=opt_f,
121
+ mode="nearest-exact",
122
+ )
123
+ for xi in z],
124
+ dim=0,
125
+ ).cpu()
126
+ except Exception:
127
+ return None
sd-webui-ultimate-tiled-vae/tiled_vae/groupnorm.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GroupNorm parameter management for tiled VAE.
3
+
4
+ Three sync modes:
5
+ EXACT – collect stats from every tile, compute weighted average, apply globally.
6
+ APPROX – stats come from a downsampled full-image estimate; each tile uses those.
7
+ NONE – each tile normalises itself independently (may produce seams).
8
+ """
9
+
10
+ from __future__ import annotations
11
+ from typing import Callable, List, Optional, Tuple
12
+
13
+ import torch
14
+ import torch.nn.functional as F
15
+
16
+ from .config import GNSyncMode
17
+ from .compat import get_devices_module
18
+
19
+
20
+ # ── Low-level helpers ─────────────────────────────────────────────────────────
21
+
22
+ def get_var_mean(
23
+ x: torch.Tensor,
24
+ num_groups: int = 32,
25
+ eps: float = 1e-6,
26
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
27
+ """
28
+ Compute per-group variance and mean.
29
+ Falls back to fp32 if fp16 overflows.
30
+ """
31
+ b, c = x.size(0), x.size(1)
32
+ channel_in_group = c // num_groups
33
+ reshaped = x.contiguous().view(
34
+ 1, b * num_groups, channel_in_group, *x.size()[2:]
35
+ )
36
+ var, mean = torch.var_mean(reshaped, dim=[0, 2, 3, 4], unbiased=False)
37
+ if var.dtype == torch.float16 and var.isinf().any():
38
+ reshaped_fp32 = reshaped.float()
39
+ var, mean = torch.var_mean(reshaped_fp32, dim=[0, 2, 3, 4], unbiased=False)
40
+ return var, mean
41
+
42
+
43
+ def custom_group_norm(
44
+ x: torch.Tensor,
45
+ num_groups: int,
46
+ mean: torch.Tensor,
47
+ var: torch.Tensor,
48
+ weight: Optional[torch.Tensor] = None,
49
+ bias: Optional[torch.Tensor] = None,
50
+ eps: float = 1e-6,
51
+ ) -> torch.Tensor:
52
+ """Apply group norm using pre-computed mean/var."""
53
+ b, c = x.size(0), x.size(1)
54
+ channel_in_group = c // num_groups
55
+ reshaped = x.contiguous().view(
56
+ 1, b * num_groups, channel_in_group, *x.size()[2:]
57
+ )
58
+ out = F.batch_norm(
59
+ reshaped, mean.to(x), var.to(x),
60
+ weight=None, bias=None,
61
+ training=False, momentum=0, eps=eps,
62
+ )
63
+ out = out.view(b, c, *x.size()[2:])
64
+ if weight is not None:
65
+ out = out * weight.view(1, -1, 1, 1)
66
+ if bias is not None:
67
+ out = out + bias.view(1, -1, 1, 1)
68
+ return out
69
+
70
+
71
+ # ── EXACT mode: GroupNormParam accumulator ────────────────────────────────────
72
+
73
+ class GroupNormParam:
74
+ """
75
+ Accumulates per-tile GroupNorm statistics and computes a pixel-weighted
76
+ global mean/var for exact cross-tile normalisation.
77
+ """
78
+
79
+ def __init__(self) -> None:
80
+ self.var_list: List[torch.Tensor] = []
81
+ self.mean_list: List[torch.Tensor] = []
82
+ self.pixel_list: List[int] = []
83
+ self.weight: Optional[torch.Tensor] = None
84
+ self.bias: Optional[torch.Tensor] = None
85
+
86
+ def add_tile(self, tile: torch.Tensor, layer) -> None:
87
+ var, mean = get_var_mean(tile, 32)
88
+ # fp16 overflow guard
89
+ if var.dtype == torch.float16 and var.isinf().any():
90
+ fp32_tile = tile.float()
91
+ var, mean = get_var_mean(fp32_tile, 32)
92
+ if var.device.type == "mps":
93
+ var = torch.clamp(var, 0, 60000).half()
94
+ mean = mean.half()
95
+ self.var_list.append(var)
96
+ self.mean_list.append(mean)
97
+ self.pixel_list.append(tile.shape[2] * tile.shape[3])
98
+ if hasattr(layer, "weight"):
99
+ self.weight = layer.weight
100
+ self.bias = layer.bias
101
+ else:
102
+ self.weight = None
103
+ self.bias = None
104
+
105
+ def summary(self) -> Optional[Callable]:
106
+ """Return a GN function using the weighted average stats, or None."""
107
+ if not self.var_list:
108
+ return None
109
+ devices = get_devices_module()
110
+ var = torch.vstack(self.var_list)
111
+ mean = torch.vstack(self.mean_list)
112
+ max_px = max(self.pixel_list)
113
+ pix = torch.tensor(
114
+ self.pixel_list, dtype=torch.float32, device=devices.device
115
+ ) / max_px
116
+ pix = pix.unsqueeze(1) / pix.sum()
117
+ var = (var * pix).sum(dim=0)
118
+ mean = (mean * pix).sum(dim=0)
119
+ w, b = self.weight, self.bias
120
+ return lambda x: custom_group_norm(x, 32, mean, var, w, b)
121
+
122
+ @staticmethod
123
+ def from_tile(tile: torch.Tensor, norm_layer) -> Callable:
124
+ """Create a single-tile GN function without accumulation."""
125
+ var, mean = get_var_mean(tile, 32)
126
+ if var.dtype == torch.float16 and var.isinf().any():
127
+ fp32_tile = tile.float()
128
+ var, mean = get_var_mean(fp32_tile, 32)
129
+ if var.device.type == "mps":
130
+ var = torch.clamp(var, 0, 60000).half()
131
+ mean = mean.half()
132
+ weight = getattr(norm_layer, "weight", None)
133
+ bias = getattr(norm_layer, "bias", None)
134
+
135
+ def _fn(x, _mean=mean, _var=var, _w=weight, _b=bias):
136
+ return custom_group_norm(x, 32, _mean, _var, _w, _b)
137
+ return _fn
138
+
139
+
140
+ # ── APPROX mode: pre-computed plan applied round-robin ───────────────────────
141
+
142
+ class ApproxGNPlan:
143
+ """
144
+ Stores (var, mean) pairs collected on the downsampled image.
145
+ Each tile pops them in order during execution.
146
+ """
147
+
148
+ def __init__(self) -> None:
149
+ self._plan: List[Tuple[torch.Tensor, torch.Tensor]] = []
150
+ self._index: int = 0
151
+
152
+ def record(self, var: torch.Tensor, mean: torch.Tensor) -> None:
153
+ self._plan.append((var, mean))
154
+
155
+ def next_stats(self) -> Tuple[torch.Tensor, torch.Tensor]:
156
+ var, mean = self._plan[self._index % len(self._plan)]
157
+ self._index += 1
158
+ return var, mean
159
+
160
+ def reset_counter(self) -> None:
161
+ self._index = 0
162
+
163
+ def __len__(self) -> int:
164
+ return len(self._plan)
sd-webui-ultimate-tiled-vae/tiled_vae/hook.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VAEHook – replaces encoder.forward / decoder.forward with tiled processing.
3
+
4
+ Key invariants:
5
+ 1. net.original_forward is saved exactly once per attach; never lost.
6
+ 2. Reentrancy guard prevents recursive hook invocation.
7
+ 3. VAE device is always restored in a finally block.
8
+ 4. NaN retry fires ONLY when has_nans(result) is True.
9
+ 5. scale factor comes from get_vae_scale_factor(), not hardcoded 8.
10
+ """
11
+
12
+ from __future__ import annotations
13
+ from typing import Optional, TYPE_CHECKING
14
+
15
+ import torch
16
+
17
+ from .config import GNSyncMode
18
+ from .tiling import split_tiles, crop_valid_region
19
+ from .taskqueue import build_task_queue, clone_task_queue
20
+ from .executor import execute_tiles, _prepare_fast_mode
21
+ from .fallback import (
22
+ has_nans, retry_in_fp32, apply_fallback, get_cheap_approx_result
23
+ )
24
+ from .logging_utils import info, warn, timed_block
25
+ from .compat import get_devices_module, get_optimal_device, get_vae_scale_factor
26
+
27
+ if TYPE_CHECKING:
28
+ from .config import TiledVAEConfig
29
+
30
+
31
+ # ── Interrupt flag ────────────────────────────────────────────────────────────
32
+
33
+ class _InterruptFlag:
34
+ @property
35
+ def interrupted(self) -> bool:
36
+ try:
37
+ from modules.shared import state
38
+ return state.interrupted
39
+ except Exception:
40
+ return False
41
+
42
+
43
+ _INTERRUPT = _InterruptFlag()
44
+
45
+
46
+ # ── VAEHook ───────────────────────────────────────────────────────────────────
47
+
48
+ class VAEHook:
49
+ """Callable that replaces encoder.forward / decoder.forward."""
50
+
51
+ _active: set = set() # reentrancy guard (uses id(self))
52
+
53
+ def __init__(self, net, cfg: "TiledVAEConfig", is_decoder: bool, scale: int = 8):
54
+ self.net = net
55
+ self.cfg = cfg
56
+ self.is_decoder = is_decoder
57
+ self.scale = scale # e.g. 8 for SD1.x/SDXL standard VAE
58
+
59
+ self.tile_size = cfg.decoder_tile_size if is_decoder else cfg.encoder_tile_size
60
+ self.pad = cfg.decoder_pad_size if is_decoder else cfg.encoder_pad_size
61
+ self.fast_mode = (cfg.fast_decoder and is_decoder) or \
62
+ (cfg.fast_encoder and not is_decoder)
63
+ self.color_fix = cfg.encoder_color_fix and not is_decoder
64
+
65
+ # ── Entry point ───────────────────────────────────────────────────────────
66
+
67
+ def __call__(self, x: torch.Tensor) -> torch.Tensor:
68
+ if id(self) in VAEHook._active:
69
+ return self.net.original_forward(x)
70
+ VAEHook._active.add(id(self))
71
+
72
+ orig_device = next(self.net.parameters()).device
73
+ try:
74
+ if self.cfg.move_vae_to_gpu:
75
+ target = get_optimal_device()
76
+ if orig_device != target:
77
+ self.net = self.net.to(target)
78
+ return self._forward_with_nan_retry(x)
79
+ finally:
80
+ VAEHook._active.discard(id(self))
81
+ if self.cfg.move_vae_to_gpu:
82
+ try:
83
+ self.net = self.net.to(orig_device)
84
+ except Exception:
85
+ pass
86
+
87
+ # ── Smart-ignore small images ─────────────────────────────────────────────
88
+
89
+ def _is_too_small(self, H: int, W: int) -> bool:
90
+ if not self.cfg.smart_ignore:
91
+ return False
92
+ if max(H, W) <= self.pad * 2 + self.tile_size:
93
+ info("Image fits in one tile – skipping tiled VAE.")
94
+ return True
95
+ return False
96
+
97
+ # ── NaN-aware retry wrapper ───────────────────────────────────────────────
98
+
99
+ def _forward_with_nan_retry(self, x: torch.Tensor) -> torch.Tensor:
100
+ cfg = self.cfg
101
+ label = "Decoder" if self.is_decoder else "Encoder"
102
+
103
+ result = None
104
+ try:
105
+ with timed_block(label, enabled=cfg.log_timings):
106
+ result = self._tiled_forward(x)
107
+ except Exception as exc:
108
+ # Non-NaN exception: go straight to fallback policy
109
+ approx = get_cheap_approx_result(x, self.is_decoder)
110
+ return apply_fallback(cfg, x, self.net, self.net.original_forward,
111
+ approx, exc, self.is_decoder)
112
+
113
+ # Result obtained – check for NaN
114
+ if result is not None and cfg.retry_fp32_on_nan and has_nans(result):
115
+ retried = retry_in_fp32(self._tiled_forward, x, self.net)
116
+ if retried is not None:
117
+ return retried
118
+ # retry also NaN or failed – apply fallback
119
+ approx = get_cheap_approx_result(x, self.is_decoder)
120
+ exc = RuntimeError("NaN in tiled VAE output even after fp32 retry.")
121
+ return apply_fallback(cfg, x, self.net, self.net.original_forward,
122
+ approx, exc, self.is_decoder)
123
+
124
+ return result
125
+
126
+ # ── Core tiled forward ────────────────────────────────────────────────────
127
+
128
+ @torch.no_grad()
129
+ def _tiled_forward(self, z: torch.Tensor) -> torch.Tensor:
130
+ cfg = self.cfg
131
+ net = self.net
132
+ device = next(net.parameters()).device
133
+ dtype = next(net.parameters()).dtype
134
+ is_decoder = self.is_decoder
135
+ scale = self.scale
136
+
137
+ z = z.detach()
138
+ N, C, H, W = z.shape
139
+
140
+ if self._is_too_small(H, W):
141
+ return net.original_forward(z)
142
+
143
+ # ── Scale-alignment guard ─────────────────────────────────────────────
144
+ # For the encoder: H and W must be divisible by `scale` so that H//scale
145
+ # produces the correct latent dimensions. A1111 normally ensures this,
146
+ # but we guard explicitly to surface any mismatch rather than produce a
147
+ # silent shape error downstream.
148
+ if not is_decoder and (H % scale != 0 or W % scale != 0):
149
+ warn(
150
+ f"Encoder input {W}x{H} not divisible by scale={scale}. "
151
+ "Falling back to original forward."
152
+ )
153
+ return net.original_forward(z)
154
+
155
+ info(
156
+ f"Input {W}x{H}px | tile={self.tile_size} pad={self.pad} "
157
+ f"scale={scale} mode={'fast' if self.fast_mode else 'exact'} "
158
+ f"GN={cfg.gn_sync_mode}"
159
+ )
160
+
161
+ # ── Tile plan ─────────────────────────────────────────────────────────
162
+ in_bboxes, out_bboxes = split_tiles(
163
+ H, W,
164
+ self.tile_size,
165
+ self.pad,
166
+ is_decoder,
167
+ auto_adjust=cfg.auto_adjust_tile,
168
+ scale=scale,
169
+ )
170
+ num_tiles = len(in_bboxes)
171
+
172
+ # ── Tiles in CPU RAM ──────────────────────────────────────────────────
173
+ tiles = [
174
+ z[:, :, bb[2]:bb[3], bb[0]:bb[1]].cpu()
175
+ for bb in in_bboxes
176
+ ]
177
+
178
+ # ── Safety net for fallback ───────────────────────────────────────────
179
+ result_approx = get_cheap_approx_result(z, is_decoder)
180
+
181
+ # ── Task queues ───────────────────────────────────────────────────────
182
+ base_queue = build_task_queue(net, is_decoder)
183
+
184
+ if self.fast_mode:
185
+ base_queue = _prepare_fast_mode(
186
+ z, self.tile_size, base_queue,
187
+ self.color_fix, device,
188
+ )
189
+
190
+ task_queues = [clone_task_queue(base_queue) for _ in range(num_tiles)]
191
+ del z
192
+
193
+ # ── Output shape ──────────────────────────────────────────────────────
194
+ if is_decoder:
195
+ # Decoder output channels are always 3 (RGB)
196
+ out_shape = (N, 3, H * scale, W * scale)
197
+ else:
198
+ out_ch = net.conv_out.out_channels
199
+ out_shape = (N, out_ch, H // scale, W // scale)
200
+
201
+ # ── Execute ───────────────────────────────────────────────────────────
202
+ result = execute_tiles(
203
+ tiles=tiles,
204
+ task_queues=task_queues,
205
+ in_bboxes=in_bboxes,
206
+ out_bboxes=out_bboxes,
207
+ result_shape=out_shape,
208
+ device=device,
209
+ dtype=dtype,
210
+ is_decoder=is_decoder,
211
+ gn_sync_mode=cfg.gn_sync_mode,
212
+ cfg=cfg,
213
+ interrupted_flag=_INTERRUPT,
214
+ scale=scale,
215
+ )
216
+
217
+ if result is None:
218
+ info("Generation interrupted during tiled VAE.")
219
+ if result_approx is not None:
220
+ return result_approx.to(device, dtype=dtype)
221
+ raise RuntimeError("Interrupted; no approximation available.")
222
+
223
+ return result.to(dtype)
224
+
225
+
226
+ # ── Hook lifecycle ────────────────────────────────────────────────────────────
227
+
228
+ def attach_hooks(vae, cfg: "TiledVAEConfig") -> None:
229
+ """Attach VAEHook to encoder and decoder. Safe to call multiple times."""
230
+ scale = get_vae_scale_factor(vae)
231
+ encoder = vae.encoder
232
+ decoder = vae.decoder
233
+
234
+ # Save original forwards exactly once (survives re-attach)
235
+ if not hasattr(encoder, "original_forward"):
236
+ encoder.original_forward = encoder.forward
237
+ if not hasattr(decoder, "original_forward"):
238
+ decoder.original_forward = decoder.forward
239
+
240
+ encoder.forward = VAEHook(encoder, cfg, is_decoder=False, scale=scale)
241
+ decoder.forward = VAEHook(decoder, cfg, is_decoder=True, scale=scale)
242
+ info(f"Hooks attached (scale={scale}).")
243
+
244
+
245
+ def detach_hooks(vae) -> None:
246
+ """Restore original forwards. Safe to call even if not hooked."""
247
+ for part in (vae.encoder, vae.decoder):
248
+ if isinstance(part.forward, VAEHook):
249
+ part.forward.net = None # break ref cycle
250
+ if hasattr(part, "original_forward"):
251
+ part.forward = part.original_forward
252
+ info("Hooks removed.")
253
+
254
+
255
+ def is_hooked(vae) -> bool:
256
+ return isinstance(vae.encoder.forward, VAEHook)
sd-webui-ultimate-tiled-vae/tiled_vae/logging_utils.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Logging utilities for Ultimate Tiled VAE.
3
+ All print/tqdm calls go through here so they can be silenced cleanly.
4
+ """
5
+
6
+ import gc
7
+ import time
8
+ from contextlib import contextmanager
9
+ from typing import Optional
10
+
11
+
12
+ PREFIX = "[Ultimate Tiled VAE]"
13
+
14
+
15
+ def info(msg: str) -> None:
16
+ print(f"{PREFIX} {msg}")
17
+
18
+
19
+ def warn(msg: str) -> None:
20
+ print(f"{PREFIX} WARNING: {msg}")
21
+
22
+
23
+ def error(msg: str) -> None:
24
+ print(f"{PREFIX} ERROR: {msg}")
25
+
26
+
27
+ def debug(msg: str, enabled: bool = False) -> None:
28
+ if enabled:
29
+ print(f"{PREFIX} DEBUG: {msg}")
30
+
31
+
32
+ @contextmanager
33
+ def timed_block(label: str, enabled: bool = True):
34
+ """Context manager that prints elapsed time + peak VRAM when done."""
35
+ if not enabled:
36
+ yield
37
+ return
38
+
39
+ try:
40
+ import torch
41
+ import modules.devices as devices
42
+ has_cuda = torch.cuda.is_available()
43
+ if has_cuda:
44
+ torch.cuda.reset_peak_memory_stats(devices.device)
45
+ gc.collect()
46
+ except Exception:
47
+ has_cuda = False
48
+
49
+ t0 = time.time()
50
+ yield
51
+
52
+ elapsed = time.time() - t0
53
+ try:
54
+ if has_cuda:
55
+ import torch
56
+ import modules.devices as devices
57
+ vram_mb = torch.cuda.max_memory_allocated(devices.device) / 2 ** 20
58
+ info(f"{label}: {elapsed:.3f}s, peak VRAM {vram_mb:.1f} MB")
59
+ else:
60
+ info(f"{label}: {elapsed:.3f}s")
61
+ except Exception:
62
+ info(f"{label}: {elapsed:.3f}s")
63
+
64
+
65
+ def tile_summary(num_h: int, num_w: int, real_h: int, real_w: int, tile_size: int) -> None:
66
+ total = num_h * num_w
67
+ info(
68
+ f"Splitting into {num_h}×{num_w} = {total} tiles. "
69
+ f"Effective tile {real_w}×{real_h}px (requested {tile_size}×{tile_size}px)"
70
+ )
sd-webui-ultimate-tiled-vae/tiled_vae/taskqueue.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Task-queue builder for Ultimate Tiled VAE.
3
+
4
+ Each task is a 2-element list [name, callable_or_data] so that
5
+ executor.py can mutate the second element in-place (e.g. stash residuals).
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from typing import Any, Callable, List
10
+
11
+ import torch
12
+ import torch.nn.functional as F
13
+
14
+ from .compat import get_attn_func
15
+
16
+
17
+ # ── Inplace non-linearity ─────────────────────────────────────────────────────
18
+
19
+ def _silu(x: torch.Tensor) -> torch.Tensor:
20
+ return F.silu(x, inplace=True)
21
+
22
+
23
+ # ── Block → tasks ─────────────────────────────────────────────────────────────
24
+
25
+ def _attn2task(queue: List, block: Any) -> None:
26
+ attn_fn = get_attn_func(block)
27
+ queue.append(["store_res", lambda x: x])
28
+ queue.append(["pre_norm", block.norm])
29
+ queue.append(["attn", lambda x, _b=block, _fn=attn_fn: _fn(_b, x)])
30
+ queue.append(["add_res", None])
31
+
32
+
33
+ def _resblock2task(queue: List, block: Any) -> None:
34
+ # Shortcut
35
+ if block.in_channels != block.out_channels:
36
+ if block.use_conv_shortcut:
37
+ queue.append(["store_res", block.conv_shortcut])
38
+ else:
39
+ queue.append(["store_res", block.nin_shortcut])
40
+ else:
41
+ queue.append(["store_res", lambda x: x])
42
+
43
+ queue.append(["pre_norm", block.norm1])
44
+ queue.append(["silu", _silu])
45
+ queue.append(["conv1", block.conv1])
46
+ queue.append(["pre_norm", block.norm2])
47
+ queue.append(["silu", _silu])
48
+ queue.append(["conv2", block.conv2])
49
+ queue.append(["add_res", None])
50
+
51
+
52
+ def _build_sampling(queue: List, net: Any, is_decoder: bool) -> None:
53
+ """Append the sampling/upsampling ladder tasks to the queue."""
54
+ if is_decoder:
55
+ _resblock2task(queue, net.mid.block_1)
56
+ _attn2task(queue, net.mid.attn_1)
57
+ _resblock2task(queue, net.mid.block_2)
58
+ res_iter = reversed(range(net.num_resolutions))
59
+ block_ids = net.num_res_blocks + 1
60
+ edge_level = 0
61
+ module = net.up
62
+ sample_key = "upsample"
63
+ else:
64
+ res_iter = range(net.num_resolutions)
65
+ block_ids = net.num_res_blocks
66
+ edge_level = net.num_resolutions - 1
67
+ module = net.down
68
+ sample_key = "downsample"
69
+
70
+ for i_level in res_iter:
71
+ for i_block in range(block_ids):
72
+ _resblock2task(queue, module[i_level].block[i_block])
73
+ if i_level != edge_level:
74
+ queue.append([sample_key, getattr(module[i_level], sample_key)])
75
+
76
+ if not is_decoder:
77
+ _resblock2task(queue, net.mid.block_1)
78
+ _attn2task(queue, net.mid.attn_1)
79
+ _resblock2task(queue, net.mid.block_2)
80
+
81
+
82
+ # ── Public API ────────────────────────────────────────────────────────────────
83
+
84
+ def build_task_queue(net: Any, is_decoder: bool) -> List:
85
+ """
86
+ Build a full task queue for one forward pass of `net`.
87
+ Each item is [task_name, callable_or_tensor_or_None].
88
+ """
89
+ queue: List = []
90
+ queue.append(["conv_in", net.conv_in])
91
+ _build_sampling(queue, net, is_decoder)
92
+
93
+ if not is_decoder or not net.give_pre_end:
94
+ queue.append(["pre_norm", net.norm_out])
95
+ queue.append(["silu", _silu])
96
+ queue.append(["conv_out", net.conv_out])
97
+ if is_decoder and net.tanh_out:
98
+ queue.append(["tanh", torch.tanh])
99
+
100
+ return queue
101
+
102
+
103
+ def clone_task_queue(queue: List) -> List:
104
+ """Deep-copy a task queue (so each tile gets its own mutable copy)."""
105
+ return [[item for item in task] for task in queue]
sd-webui-ultimate-tiled-vae/tiled_vae/tiling.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tile planner for Ultimate Tiled VAE.
3
+
4
+ Key improvements over original multidiffusion:
5
+ - scale factor passed explicitly (no hardcoded 8)
6
+ - _even_partition() ensures last tile <= first tile (prevents late OOM)
7
+ - Long/thin image edge cases handled
8
+ """
9
+
10
+ from __future__ import annotations
11
+ import math
12
+ from typing import List, Tuple
13
+
14
+ from .logging_utils import tile_summary
15
+
16
+ # (x1, x2, y1, y2) — x1/y1 inclusive, x2/y2 exclusive
17
+ BBox = Tuple[int, int, int, int]
18
+
19
+
20
+ def _best_tile_size(lower: int, upper: int) -> int:
21
+ """Snap `lower` up to nearest power-of-2 multiple within `upper`."""
22
+ for div in (32, 16, 8, 4, 2):
23
+ rem = lower % div
24
+ if rem == 0:
25
+ return lower
26
+ candidate = lower - rem + div
27
+ if candidate <= upper:
28
+ return candidate
29
+ return lower
30
+
31
+
32
+ def _even_partition(total: int, n_tiles: int, pad: int) -> List[int]:
33
+ """
34
+ Partition (total - 2*pad) pixels into n_tiles segments so that
35
+ no segment is larger than any earlier one.
36
+ Extra pixels go to the FIRST tiles, never the last.
37
+ """
38
+ usable = total - 2 * pad
39
+ if usable <= 0:
40
+ return [total] * n_tiles
41
+ base = usable // n_tiles
42
+ extra = usable % n_tiles
43
+ return [base + (1 if i < extra else 0) for i in range(n_tiles)]
44
+
45
+
46
+ def split_tiles(
47
+ h: int,
48
+ w: int,
49
+ tile_size: int,
50
+ pad: int,
51
+ is_decoder: bool,
52
+ auto_adjust: bool = True,
53
+ scale: int = 8,
54
+ ) -> Tuple[List[BBox], List[BBox]]:
55
+ """
56
+ Compute input and output bounding boxes for all tiles.
57
+
58
+ Parameters
59
+ ----------
60
+ h, w : height / width of the input tensor
61
+ tile_size : target tile edge length (pixels, in input space)
62
+ pad : overlap padding around each tile (input space)
63
+ is_decoder : True → output = input × scale; False → output = input ÷ scale
64
+ auto_adjust : shrink tiles evenly so last tile <= first tile
65
+ scale : VAE spatial scale factor (default 8; use get_vae_scale_factor())
66
+
67
+ Returns
68
+ -------
69
+ in_bboxes : padded input regions
70
+ out_bboxes : valid output regions (in output space)
71
+ """
72
+ usable_h = max(h - 2 * pad, 1)
73
+ usable_w = max(w - 2 * pad, 1)
74
+ num_h = max(math.ceil(usable_h / tile_size), 1)
75
+ num_w = max(math.ceil(usable_w / tile_size), 1)
76
+
77
+ if auto_adjust:
78
+ raw_th = math.ceil(usable_h / num_h)
79
+ raw_tw = math.ceil(usable_w / num_w)
80
+ real_th = _best_tile_size(raw_th, tile_size)
81
+ real_tw = _best_tile_size(raw_tw, tile_size)
82
+ else:
83
+ real_th = tile_size
84
+ real_tw = tile_size
85
+
86
+ tile_summary(num_h, num_w, real_th, real_tw, tile_size)
87
+
88
+ # Even partition: last tile never larger than first
89
+ seg_h = _even_partition(h, num_h, pad)
90
+ seg_w = _even_partition(w, num_w, pad)
91
+
92
+ starts_h = [pad + sum(seg_h[:i]) for i in range(num_h)]
93
+ starts_w = [pad + sum(seg_w[:i]) for i in range(num_w)]
94
+
95
+ in_bboxes: List[BBox] = []
96
+ out_bboxes: List[BBox] = []
97
+
98
+ for i, (y0, th) in enumerate(zip(starts_h, seg_h)):
99
+ for j, (x0, tw) in enumerate(zip(starts_w, seg_w)):
100
+ y1 = min(y0 + th, h)
101
+ x1 = min(x0 + tw, w)
102
+
103
+ # Output region: extend to image boundary at edges
104
+ oy0 = 0 if y0 <= pad else y0
105
+ oy1 = h if y1 >= h - pad else y1
106
+ ox0 = 0 if x0 <= pad else x0
107
+ ox1 = w if x1 >= w - pad else x1
108
+
109
+ if is_decoder:
110
+ out_bboxes.append((ox0 * scale, ox1 * scale, oy0 * scale, oy1 * scale))
111
+ else:
112
+ out_bboxes.append((ox0 // scale, ox1 // scale, oy0 // scale, oy1 // scale))
113
+
114
+ in_bboxes.append((
115
+ max(0, x0 - pad), min(w, x1 + pad),
116
+ max(0, y0 - pad), min(h, y1 + pad),
117
+ ))
118
+
119
+ return in_bboxes, out_bboxes
120
+
121
+
122
+ def crop_valid_region(
123
+ tile: "torch.Tensor",
124
+ in_bbox: BBox,
125
+ out_bbox: BBox,
126
+ is_decoder: bool,
127
+ scale: int = 8,
128
+ ) -> "torch.Tensor":
129
+ """
130
+ Trim padded tile output to the valid (output) region.
131
+ in_bbox / out_bbox are in the coordinate spaces produced by split_tiles().
132
+ """
133
+ if is_decoder:
134
+ padded = (in_bbox[0] * scale, in_bbox[1] * scale,
135
+ in_bbox[2] * scale, in_bbox[3] * scale)
136
+ else:
137
+ padded = (in_bbox[0] // scale, in_bbox[1] // scale,
138
+ in_bbox[2] // scale, in_bbox[3] // scale)
139
+
140
+ mx0 = out_bbox[0] - padded[0] # left trim
141
+ mx1 = out_bbox[1] - padded[1] # right (≤0: from right)
142
+ my0 = out_bbox[2] - padded[2] # top trim
143
+ my1 = out_bbox[3] - padded[3] # bottom (≤0: from bottom)
144
+
145
+ H, W = tile.shape[2], tile.shape[3]
146
+ y_end = H + my1 if my1 < 0 else H
147
+ x_end = W + mx1 if mx1 < 0 else W
148
+ return tile[:, :, my0:y_end, mx0:x_end]