Upload sd-webui-ultimate-tiled-vae using SD-Hub
Browse files- sd-webui-ultimate-tiled-vae/LICENSE +16 -0
- sd-webui-ultimate-tiled-vae/README.md +70 -0
- sd-webui-ultimate-tiled-vae/scripts/__pycache__/ultimate_tiled_vae.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/scripts/ultimate_tiled_vae.py +263 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__init__.py +3 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/__init__.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/compat.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/config.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/executor.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/fallback.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/groupnorm.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/hook.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/logging_utils.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/taskqueue.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/__pycache__/tiling.cpython-310.pyc +0 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/compat.py +169 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/config.py +160 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/executor.py +251 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/fallback.py +127 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/groupnorm.py +164 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/hook.py +256 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/logging_utils.py +70 -0
- sd-webui-ultimate-tiled-vae/tiled_vae/taskqueue.py +105 -0
- 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]
|