Skip to content

Studio: size seam-free VAE tiles from measured peaks (no slower than stock, no low-VRAM OOM) - #12764

Merged
danielhanchen merged 33 commits into
mainfrom
studio/vae-tiles-perf
Oct 6, 2026
Merged

danielhanchen merged 33 commits into
mainfrom
studio/vae-tiles-perf

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Follows #12736 (merged). #12736 made every image VAE's tiled decode seam-free; this makes those wide tiles no slower than the stock tiles they replace, and closes the low-VRAM OOM corners, while keeping the seam fix.

Problem

Measured after #12736, against main before it, on real weights:

  • Qwen-Image at low free VRAM: the wide tiles are sized from one per-pixel figure that is 2 to 5x the real decode peak of most VAEs, so at about 0.6 GiB free Qwen-Image decodes at the 32-latent floor and is 1.4 to 1.8x slower than the stock 32 / 8 tiles.
  • FLUX.1 / SDXL / FLUX.2 at an edge sliver (1600 px): the 128-latent wide tiles decode about 6 ms (9%) slower than stock.
  • HunyuanImage-2.1: the budget figure is 5x its measured peak, so 2048 px rarely gets tiles above the floor, and below about 1.4 GiB free the 1.3 GiB floor tile does not fit at all and OOMs where the stock tiles would not.
  • Qwen-Image-2.1: at about 0.35 GiB free the floor tile (inherited from Studio: seam-free VAE tiles for Qwen-Image-2.1 on low-VRAM tiers (fixes thin horizontal / vertical lines) #12696) OOMs, where the stock tiles decode.

Change

diffusion_vae_tiling.py (and the fused VAE module's batched loop, so the fallback keeps it):

  • The decode budget comes from measured peaks. Each figure is bf16 MiB per latent of tile area, unfused / fused, the worst over tile sides 8 to 128, measured on the real weights:
  • At a sliver, a VAE whose stock tile is the floor tile (AutoencoderKL, FLUX.2) takes the fewest decoder calls, then the widest tile, that decode no more latents per side than the stock loop. At 1600 px that is two 120-latent tiles: 240 decoded latents, as stock, in 4 calls instead of 9. Those VAEs grow their tiles past the floor only into memory the device has free, not the allocator's cached blocks.
  • When even the floor tile cannot fit the free VRAM, the decode uses the stock tiles and logs why (once per VAE). This only applies to a VAE whose stock tile is smaller than its floor tile: HunyuanImage (1.3 GiB against 0.2) and Qwen-Image-2.1 (0.43 GiB fused). An OOM in the wide tiles retries at the floor tile, then in the stock tiles. Losing quality is better than an OOM.
  • When the wide path falls back to stock tiles on the Wan family, it keeps diffusion_vae_fused's batched tile loop, as fast as before Studio: seam-free tiled decode for every image VAE whose stock tiles fall under the floor (HunyuanImage-2.1 grid, Qwen-Image, slivers) #12736.
  • The geometry is read once at install, and the blend weights are cached per VAE.

Performance

Arms: pre-#12736 is main before #12736 (df4eb29579). #12696 is 27916a55fb. #12736 is acc589fbe5, the code main carries now (the merged commit only tightened comments). Head is this PR (a4a70e3a74, the decode path is unchanged by the merges since).

Decode alone, emulated free VRAM

Each VAE's tiled decode of a real latent (the untiled encode of DIV2K 0010), with Studio's fused VAE kernels installed in load order. Exclusive B200. mem_get_info is capped at the given free memory with no allocator slack. The cap is not enforced, so a #12736 figure above the free memory would OOM on a real card. Arms are interleaved in one process. Median of 9 decodes after one warm decode (15 for the AutoencoderKL VAEs), in seconds. PSNR and the worst 64 px window are against the untiled decode of the same latent.

VAE size free GiB pre-#12736 s #12736 s head s head tile peak MiB, pre-#12736 / #12736 / head PSNR dB, pre-#12736 / #12736 / head window, pre-#12736 / #12736 / head
Qwen-Image 1024 0.35 0.186 0.254 0.148 44x32 189 / 192 / 260 38.0 / 38.57 / 38.83 7.74 / 7.9 / 7.51
Qwen-Image 1024 0.6 0.175 0.244 0.077 54x44 189 / 192 / 430 38.0 / 38.57 / 39.11 7.74 / 7.9 / 7.16
Qwen-Image 1024 2.0 0.186 0.047 0.034 72x72 189 / 570 / 924 38.0 / 39.52 / 40.35 7.74 / 6.85 / 6.84
Qwen-Image 1664 0.35 0.406 0.740 0.494 40x32 203 / 212 / 257 41.36 / 41.91 / 41.99 9.21 / 8.74 / 8.69
Qwen-Image 1664 0.6 0.415 0.714 0.194 48x48 203 / 212 / 437 41.36 / 41.91 / 42.28 9.21 / 8.74 / 8.48
Qwen-Image 1664 2.0 0.384 0.107 0.049 112x64 203 / 651 / 1,294 41.36 / 42.71 / 43.18 9.21 / 7.91 / 7.32
Qwen-Image 2048 0.35 0.601 1.049 0.952 38x32 217 / 228 / 262 46.31 / 47.02 / 47.0 6.31 / 5.85 / 6.05
Qwen-Image 2048 0.6 0.582 1.065 0.296 56x40 217 / 228 / 442 46.31 / 47.02 / 47.29 6.31 / 5.85 / 6.18
Qwen-Image 2048 2.0 0.581 0.189 0.074 96x76 217 / 663 / 1,332 46.31 / 47.55 / 47.99 6.31 / 5.94 / 5.61
Qwen-Image-2.1 1024 0.35 0.287 0.108 0.319 stock 120 / 449 / 120 35.71 / 50.57 / 35.71 12.38 / 1.19 / 12.38
Qwen-Image-2.1 1024 0.6 0.308 0.110 0.112 32x32 120 / 449 / 449 35.71 / 50.57 / 50.57 12.38 / 1.19 / 1.19
Qwen-Image-2.1 1024 2.0 0.297 0.105 0.066 64x40 120 / 449 / 1,099 35.71 / 50.57 / 55.51 12.38 / 1.19 / 0.67
Qwen-Image-2.1 2048 0.35 0.687 0.504 0.739 stock 158 / 497 / 158 37.98 / 52.1 / 37.98 10.17 / 1.17 / 10.17
Qwen-Image-2.1 2048 0.6 0.775 0.505 0.512 32x32 158 / 497 / 497 37.98 / 52.1 / 52.1 10.17 / 1.17 / 1.17
Qwen-Image-2.1 2048 2.0 0.898 0.508 0.259 72x44 158 / 497 / 1,405 37.98 / 52.1 / 55.53 10.17 / 1.17 / 0.85
HunyuanImage-2.1 1024 0.6 0.234 0.077 0.222 stock 187 / 1,280 / 187 21.79 / 99.0 / 21.79 47.11 / 0.0 / 47.11
HunyuanImage-2.1 1024 2.0 0.231 0.078 0.093 32x32 187 / 1,280 / 1,280 21.79 / 99.0 / 99.0 47.11 / 0.0 / 0.0
HunyuanImage-2.1 2048 0.6 0.984 0.651 1.016 stock 213 / 1,332 / 213 23.01 / 28.28 / 23.01 36.87 / 28.19 / 36.87
HunyuanImage-2.1 2048 2.0 0.896 0.648 0.657 32x32 213 / 1,332 / 1,332 23.01 / 28.28 / 28.28 36.87 / 28.19 / 28.19
HunyuanImage-2.1 2048 4.0 0.899 0.651 0.447 64x32 213 / 1,332 / 2,616 23.01 / 28.28 / 32.81 36.87 / 28.19 / 14.75
FLUX.1 ae 1600 0.6 0.066 0.069 0.066 120x120 1,408 / 1,441 / 1,270 40.11 / 43.83 / 42.85 6.15 / 3.48 / 3.48
FLUX.1 ae 1600 4.0 0.066 0.068 0.051 120x200 1,408 / 1,441 / 2,097 40.11 / 43.83 / 49.68 6.15 / 3.48 / 1.89
FLUX.1 ae 2048 0.6 0.106 0.107 0.106 stock 1,429 / 1,429 / 1,429 39.29 / 39.29 / 39.29 8.42 / 8.42 / 8.42
SDXL 1600 0.6 0.066 0.069 0.065 120x120 1,408 / 1,441 / 1,270 41.27 / 43.43 / 42.5 5.23 / 3.13 / 4.5
SDXL 1600 4.0 0.067 0.072 0.057 120x200 1,408 / 1,441 / 2,097 41.27 / 43.43 / 46.8 5.23 / 3.13 / 2.03
FLUX.2 1600 0.6 0.066 0.072 0.069 120x120 1,409 / 1,442 / 1,271 29.98 / 32.56 / 31.75 14.53 / 9.96 / 9.35
FLUX.2 1600 4.0 0.067 0.071 0.055 120x200 1,409 / 1,442 / 2,099 29.98 / 32.56 / 38.12 14.53 / 9.96 / 4.86

End to end through Studio's DiffusionBackend

In-process DiffusionBackend.load_pipeline / generate on an exclusive B200, with transformer_quant=auto and every other setting at its default. Studio's 16, 12 and 8 GB tiers are simulated: mem_get_info reports that total, and the allocator is capped with set_per_process_memory_fraction. Each process is fresh, with fresh compile caches. The host was over-subscribed for part of the run (load average up to 780 on 192 cores), so every arm of a tier is loaded at once and the renders run one at a time, with the arm order rotating each repeat. Host load then lands on every arm alike. Pre-#12736 runs twice per tier (a and b) as the A/A. Per size: 1 cold render, then 5 warm. The table is the median of warm renders 2 to 5. Steps: 6 (Qwen-Image, Qwen-Image-2.1, HunyuanImage-2.1, FLUX.1-dev), 8 (SDXL), 4 (FLUX.2-klein). Peak alloc is torch.cuda.max_memory_allocated over the image. Device is this process's NVML memory. RSS is the host resident peak. PSNR and window are measured on the output PNG against the untiled decode of the arm's own latent. The latent is identical in every arm. Full card only for HunyuanImage, SDXL and FLUX.2-klein: no family tiles there, so every arm runs the same code.

model tier GB size total s, pre-#12736 (A/A a / b) total s, head decode s median (min), pre-#12736 decode s median (min), head peak alloc MiB, pre-#12736 / head device MiB, pre-#12736 / head RSS MiB, pre-#12736 / head head decode tile PSNR dB, pre-#12736 / head window, pre-#12736 / head
Qwen-Image-2.1 16 1024x1024 0.79 (0.78 / 0.80) 0.60 0.227 (0.189) 0.049 (0.032) 9,511 / 10,549 11,836 / 13,022 18,597 / 18,584 64x64 45.57 / 58.05 4.01 / 0.31
Qwen-Image-2.1 16 1664x1664 2.26 (2.25 / 2.27) 1.87 0.484 (0.431) 0.080 (0.078) 9,506 / 12,255 12,016 / 15,542 18,689 / 18,677 104x104 42.48 / 59.83 7.76 / 0.35
Qwen-Image-2.1 12 1024x1024 1.20 (1.19 / 1.22) 0.97 0.274 (0.252) 0.048 (0.041) 5,864 / 6,279 8,116 / 9,362 27,094 / 26,958 64x64 45.57 / 58.05 4.01 / 0.31
Qwen-Image-2.1 12 1664x1664 2.34 (2.49 / 2.33) 1.94 0.486 (0.455) 0.088 (0.081) 4,528 / 5,800 6,234 / 8,434 27,155 / 27,069 104x104 42.48 / 59.83 7.76 / 0.35
Qwen-Image-2.1 8 1024x1024 2.12 (2.04 / 2.15) 1.86 0.295 (0.206) 0.079 (0.058) 2,196 / 2,480 4,452 / 5,598 27,087 / 27,062 64x64 45.57 / 58.05 4.01 / 0.31
Qwen-Image-2.1 8 1664x1664 2.51 (2.49 / 2.53) 2.07 0.626 (0.421) 0.132 (0.115) 3,338 / 3,449 5,714 / 6,720 27,185 / 27,216 104x60 42.48 / 56.07 7.76 / 0.88
Qwen-Image 16 1024x1024 7.81 (7.82 / 7.81) 7.91 0.018 (0.014) 0.022 (0.017) 2,367 / 2,367 5,194 / 5,194 84,778 / 84,814 untiled 53.1 / 53.1 3.37 / 3.37
Qwen-Image 16 1328x1328 9.49 (11.06 / 9.49) 9.01 0.309 (0.178) 0.038 (0.022) 2,636 / 2,636 5,194 / 6,096 84,863 / 84,859 166x166 52.95 / 56.04 2.73 / 1.1
Qwen-Image 12 1024x1024 12.13 (12.95 / 10.80) 13.06 0.084 (0.077) 0.022 (0.013) 2,367 / 2,367 4,594 / 5,194 84,814 / 84,778 128x128 48.29 / 53.1 7.31 / 3.37
Qwen-Image 12 1328x1328 7.76 (7.76 / 7.78) 7.61 0.125 (0.101) 0.025 (0.021) 2,668 / 2,668 3,694 / 5,110 84,895 / 84,821 166x166 52.95 / 56.04 2.73 / 1.1
Qwen-Image 8 1024x1024 8.09 (15.13 / 7.97) 7.60 0.390 (0.207) 0.014 (0.013) 2,367 / 2,367 4,354 / 5,194 84,805 / 84,781 128x128 48.29 / 53.1 7.31 / 3.37
Qwen-Image 8 1328x1328 8.00 (8.01 / 8.00) 7.73 0.359 (0.271) 0.031 (0.025) 2,636 / 2,636 4,658 / 6,096 84,853 / 84,867 166x166 52.95 / 56.04 2.73 / 1.1
HunyuanImage-2.1 full 1024x1024 0.78 (0.78 / 0.78) 0.79 0.100 (0.099) 0.099 (0.099) 39,319 / 39,319 40,780 / 40,780 7,290 / 6,918 untiled 54.72 / 54.72 0.44 / 0.44
HunyuanImage-2.1 full 2048x2048 2.01 (2.02 / 2.00) 1.96 0.458 (0.456) 0.457 (0.456) 42,434 / 42,434 44,798 / 44,798 7,506 / 7,133 untiled 54.67 / 54.67 0.42 / 0.42
HunyuanImage-2.1 16 1024x1024 5.16 (5.12 / 5.20) 5.18 0.106 (0.099) 0.102 (0.100) 2,755 / 2,755 4,632 / 4,632 68,616 / 68,607 untiled 54.51 / 54.49 0.46 / 0.47
HunyuanImage-2.1 16 2048x2048 6.86 (6.83 / 7.17) 6.14 1.242 (0.880) 0.467 (0.460) 3,052 / 4,919 4,214 / 8,376 68,864 / 68,864 64x64 28.72 / 54.27 32.37 / 0.44
HunyuanImage-2.1 12 1024x1024 5.49 (5.54 / 5.49) 5.22 0.308 (0.223) 0.110 (0.104) 2,755 / 2,755 3,940 / 4,632 68,609 / 68,495 32x32 27.74 / 54.57 22.51 / 0.47
HunyuanImage-2.1 12 2048x2048 6.32 (6.28 / 6.32) 8.19 1.120 (0.956) 3.154 (0.477) 3,084 / 4,951 4,324 / 8,488 68,804 / 68,712 64x64 28.72 / 54.32 32.39 / 0.44
HunyuanImage-2.1 8 1024x1024 5.26 (5.26 / 5.30) 5.10 0.248 (0.206) 0.110 (0.103) 2,755 / 2,755 3,940 / 4,632 68,628 / 68,474 32x32 27.74 / 54.49 22.5 / 0.47
HunyuanImage-2.1 8 2048x2048 6.13 (6.18 / 6.13) 5.76 0.961 (0.887) 0.630 (0.520) 3,052 / 3,441 4,212 / 6,256 68,773 / 68,690 64x40 28.72 / 43.76 32.37 / 5.99
FLUX.1-dev 16 1024x1024 1.79 (1.78 / 1.82) 1.81 0.021 (0.019) 0.022 (0.019) 1,756 / 1,756 4,392 / 4,392 39,551 / 39,566 untiled 55.87 / 55.87 0.42 / 0.42
FLUX.1-dev 16 1600x1600 2.12 (1.90 / 2.46) 1.87 0.070 (0.068) 0.046 (0.046) 2,429 / 3,667 4,574 / 7,052 39,675 / 39,608 200x200 43.01 / 56.13 3.12 / 0.41
FLUX.1-dev 16 2048x2048 2.78 (2.59 / 3.01) 2.42 0.119 (0.113) 0.113 (0.113) 3,430 / 3,430 5,694 / 7,052 39,770 / 39,740 stock tiles 42.62 / 42.62 4.94 / 4.94
FLUX.1-dev 12 1024x1024 1.75 (1.74 / 1.76) 1.85 0.019 (0.018) 0.020 (0.018) 1,756 / 1,756 4,392 / 4,392 39,559 / 39,560 stock tiles 55.87 / 55.87 0.42 / 0.42
FLUX.1-dev 12 1600x1600 1.82 (1.85 / 1.82) 1.82 0.072 (0.066) 0.069 (0.059) 2,429 / 3,667 4,230 / 6,704 39,680 / 39,608 200x200 43.01 / 56.13 3.12 / 0.41
FLUX.1-dev 12 2048x2048 2.76 (3.29 / 2.42) 3.49 0.113 (0.112) 0.135 (0.114) 3,430 / 3,430 5,344 / 5,344 39,777 / 39,750 stock tiles 42.62 / 42.62 4.94 / 4.94
FLUX.1-dev 8 1024x1024 1.80 (1.75 / 1.86) 1.86 0.020 (0.019) 0.019 (0.018) 1,756 / 1,756 4,392 / 4,392 39,568 / 39,573 stock tiles 55.87 / 55.87 0.42 / 0.42
FLUX.1-dev 8 1600x1600 1.93 (1.82 / 2.00) 1.83 0.068 (0.067) 0.065 (0.062) 2,429 / 2,429 4,574 / 5,292 39,681 / 39,621 120x120 43.01 / 43.49 3.12 / 3.53
FLUX.1-dev 8 2048x2048 2.70 (2.41 / 2.92) 2.95 0.113 (0.112) 0.115 (0.112) 3,430 / 3,430 5,694 / 5,692 39,764 / 39,763 stock tiles 42.62 / 42.62 4.94 / 4.94
SDXL full 1024x1024 0.38 (2.85 / 0.38) 1.08 0.055 (0.018) 0.023 (0.021) 9,426 / 9,426 11,328 / 11,326 9,171 / 8,384 untiled 54.72 / 54.72 0.37 / 0.37
SDXL 16 1024x1024 3.24 (3.48 / 2.80) 2.34 0.594 (0.181) 0.494 (0.185) 6,522 / 6,522 7,484 / 7,484 12,555 / 11,462 stock tiles 54.72 / 54.72 0.37 / 0.37
SDXL 12 1024x1024 2.83 (3.22 / 2.52) 2.44 0.582 (0.174) 1.391 (0.307) 6,522 / 6,522 7,484 / 7,484 11,462 / 11,392 stock tiles 54.72 / 54.72 0.37 / 0.37
SDXL 8 1024x1024 4.10 (3.65 / 6.78) 5.95 2.109 (0.140) 1.837 (0.165) 6,522 / 6,522 7,484 / 7,484 12,661 / 12,619 stock tiles 54.72 / 54.72 0.37 / 0.37
FLUX.2-klein-4B full 1024x1024 0.23 (0.25 / 0.22) 0.21 0.036 (0.020) 0.037 (0.032) 13,222 / 13,222 15,268 / 15,268 3,099 / 3,097 untiled 54.21 / 54.21 0.41 / 0.41
FLUX.2-klein-4B full 1536x1536 0.55 (0.56 / 0.55) 0.54 0.067 (0.042) 0.062 (0.059) 15,034 / 15,034 18,424 / 18,424 3,230 / 3,230 untiled 54.44 / 54.44 0.43 / 0.43
FLUX.2-klein-4B 16 1024x1024 0.40 (0.45 / 0.39) 0.43 0.041 (0.020) 0.022 (0.019) 1,672 / 1,672 3,758 / 3,758 19,782 / 19,786 untiled 54.21 / 54.21 0.41 / 0.41
FLUX.2-klein-4B 16 1536x1536 0.57 (0.51 / 0.64) 0.47 0.057 (0.050) 0.060 (0.054) 2,526 / 2,526 3,854 / 3,854 19,889 / 19,838 stock tiles 30.44 / 30.44 12.61 / 12.61
FLUX.2-klein-4B 12 1024x1024 0.39 (1.59 / 0.38) 1.19 0.026 (0.017) 0.027 (0.020) 1,672 / 1,672 3,758 / 3,758 19,784 / 19,784 stock tiles 54.21 / 54.21 0.41 / 0.41
FLUX.2-klein-4B 12 1536x1536 0.52 (0.46 / 0.66) 0.43 0.061 (0.053) 0.054 (0.053) 2,558 / 2,558 3,986 / 3,986 19,898 / 19,897 stock tiles 30.44 / 30.44 12.61 / 12.61
FLUX.2-klein-4B 8 1024x1024 0.42 (0.42 / 0.42) 0.39 0.026 (0.018) 0.028 (0.018) 1,672 / 1,672 3,758 / 3,758 19,786 / 19,786 stock tiles 54.21 / 54.21 0.41 / 0.41
FLUX.2-klein-4B 8 1536x1536 0.45 (0.61 / 0.43) 0.42 0.054 (0.053) 0.056 (0.053) 2,526 / 2,526 3,854 / 3,854 19,891 / 19,891 stock tiles 30.44 / 30.44 12.61 / 12.61

Very low free VRAM at decode time

The same e2e processes at the 12 GB tier. Tiling is forced on, and a filler tensor holds all but 0.6 or 0.35 GiB of the simulated free memory while the VAE decodes the render's latent. Median of 3 after a warm decode, in seconds:

model size free GiB pre a / pre b #12696 head head tile PSNR dB, pre-#12736 / head window, pre-#12736 / head
Qwen-Image-2.1 1024 0.6 0.294 / 0.306 0.293 0.186 32x32 45.65 / 54.51 3.95 / 1.0
Qwen-Image-2.1 1024 0.35 0.328 / 0.207 OOM 0.329 stock 45.65 / 45.65 3.95 / 3.95
Qwen-Image-2.1 1664 0.6 0.730 / 0.808 0.567 0.309 32x32 42.48 / 50.54 7.73 / 2.3
Qwen-Image-2.1 1664 0.35 0.800 / 0.799 OOM 0.868 stock 42.48 / 42.48 7.73 / 7.73
Qwen-Image 1024 0.6 0.377 / 0.207 - 0.082 54x44 48.4 / 50.32 7.3 / 5.38
Qwen-Image 1024 0.35 0.199 / 0.220 - 0.130 44x35 48.4 / 50.33 7.3 / 5.31
Qwen-Image 1328 0.6 0.422 / 0.248 - 0.097 66x41 53.31 / 55.22 2.71 / 1.82
Qwen-Image 1328 0.35 0.346 / 0.252 - 0.197 46x35 53.31 / 54.83 2.71 / 2.08
HunyuanImage-2.1 1024 0.6 0.294 / 0.237 - 0.321 stock 27.72 / 27.72 22.6 / 22.58
HunyuanImage-2.1 2048 0.6 1.349 / 1.290 - 1.584 stock 28.7 / 28.7 32.43 / 32.42
HunyuanImage-2.1 2048 0.35 1.392 / 1.525 - 1.331 stock 28.7 / 28.7 32.43 / 32.42

Real low-VRAM cards

Colab L4 (22.5 GB), Kaggle T4 (15 GB) and Colab T4 High-RAM, plus Colab G4 (RTX PRO 6000) for HunyuanImage, which needs about 77 GB of host RAM at load, more than an L4 VM has. Studio is installed by its own install.sh. The 12 and 16 GB tiers are a real VRAM ballast plus a reported total. Pre-#12736 and head are interleaved, one after the other twice, with a fresh process each. Warm totals and decodes are medians of 4 renders, at 6 steps.

card, model tier size total s, pre-#12736 / head decode s, pre-#12736 / head decode peak MiB, pre-#12736 / head head tile PSNR dB vs untiled, pre-#12736 / head
L4, Qwen-Image-2.1 FP8 12 GB 1024 12.08 / 11.67 0.77 / 0.46 7,625 / 9,234 untiled 45.23 / 58.57
L4, Qwen-Image-2.1 FP8 12 GB 1664 40.54 / 39.61 2.02 / 1.27 4,328 / 8,757 untiled 41.48 / 57.86
L4, Qwen-Image-2.1 FP8 16 GB 1664 40.12 / 39.25 2.02 / 1.24 7,659 / 12,087 untiled 41.48 / 57.86
T4, Qwen-Image-2.1 GGUF Q4_K_M 12 GB 1024 59.37 / 58.52 2.02 / 1.15 939 / 2,447 untiled 41.58 / 69.38
T4, Qwen-Image-2.1 GGUF Q4_K_M 12 GB 1664 201.57 / 194.15 5.27 / 3.16 975 / 5,311 untiled 32.97 / 64.40
T4, Qwen-Image-2.1 GGUF Q4_K_M 15 GB, no cap 1664 198.89 / 193.34 5.34 / 3.20 5,059 / 9,397 untiled 32.97 / 64.40
L4, Qwen-Image GGUF Q4_K_M 12 GB 1024 32.84 / 32.73 0.32 / 0.14 1,055 / 1,422 untiled 53.64 / 56.09
L4, Qwen-Image GGUF Q4_K_M 12 GB 1328 48.87 / 49.38 0.37 / 0.27 1,065 / 2,213 untiled 49.62 / 53.90
L4, Qwen-Image GGUF Q4_K_M 16 GB 1328 48.72 / 48.79 0.37 / 0.27 1,065 / 2,213 untiled 49.62 / 53.90
G4, HunyuanImage-2.1 FP8 12 GB 1024 3.98 / 3.89 0.20 / 0.11 1,099 / 2,859 untiled 28.10 / 54.70
G4, HunyuanImage-2.1 FP8 12 GB 2048 5.14 / 4.95 0.83 / 0.64 1,088 / 9,005 untiled 28.19 / 54.54
G4, HunyuanImage-2.1 FP8 16 GB 2048 5.15 / 4.95 0.83 / 0.64 1,101 / 9,005 untiled 28.19 / 54.74
T4 High-RAM, FLUX.1-dev GGUF Q4_K_M 12 GB 1600 97.80 / 97.32 2.20 / 1.64 1,597 / 3,628 untiled 41.51 / 66.56
L4, FLUX.1-dev GGUF Q4_K_M, head a4a70e3a74 12 GB 1600 35.32 / 35.16 1.21 / 1.04 6,685 / 6,685 untiled 42.53 / 55.95

Tests

  • tests/test_diffusion_vae_tiling_general.py (2,388 tests, CPU, tiny diffusers VAEs of each class):
    • geometry and ratio read off AutoencoderKLQwenImage (8x), AutoencoderKLQwenImage21 (16x), AutoencoderKLHunyuanImage (32x), AutoencoderKL and AutoencoderKLFlux2 (8x);
    • the stock-layout floor per ratio, including the sliver sides;
    • wide tiles full and edge-aligned, with weights summing to 1, at every length and ratio;
    • seam reduction on Qwen-Image and HunyuanImage;
    • AutoencoderKL bit-identical to stock where its tiles meet the floor, and on the wide path for a sliver;
    • which encodes are replaced, and the Qwen-Image encode closer to untiled;
    • the kill switch per class, KEEP_STOCK, and an unreadable geometry keeping stock;
    • every image family's VAE class (read from the diffusers pipeline's vae annotation) is one the rule reads, or is listed in KEEP_STOCK.
  • Perf tests added:
    • the per-class figures bound the measured peaks and stay within 10% of them; the fused figure is used only while the fused kernels are installed and not fallen back;
    • at every sliver length from 129 to 419 latents and three budgets, an AutoencoderKL layout makes fewer decoder calls than stock, decodes no more latents per side (up to rounding), and keeps every overlap at 32 or more;
    • the 32-latent floor layout (Qwen-Image, Qwen-Image-2.1, HunyuanImage) is unchanged at every length;
    • a floor that cannot fit decodes in the stock tiles and logs once; Qwen-Image and AutoencoderKL never fall back that way, since their stock tiles need as much;
    • an OOM retries at the floor, then in the stock tiles, and other errors propagate;
    • the stock fallback keeps the fused batched loop, which takes tiled_decode back on uninstall;
    • only AutoencoderKL / FLUX.2 size past the floor without the allocator's cache;
    • geometry and blend weights are computed once per VAE.
  • On acc589fbe5 (Studio: seam-free tiled decode for every image VAE whose stock tiles fall under the floor (HunyuanImage-2.1 grid, Qwen-Image, slivers) #12736 as merged, before these commits), 298 of the perf tests fail. On the head all 2,089 of them pass.
  • tests/test_diffusion_vae_tiling.py (Studio: seam-free VAE tiles for Qwen-Image-2.1 on low-VRAM tiers (fixes thin horizontal / vertical lines) #12696, 580 tests) passes unchanged.
  • The related suites pass, 4,222 tests: diffusion backend, memory, calibrated activation, speed, fused VAE, single-frame VAE, upscale gate, VAE prebuild, channels-last, Qwen-Image-2.1 VAE layout.
  • Both tiling suites (2,968 tests) pass on torch 2.6.0 (CPU) and 2.12.1. The module uses no torchao.
  • Qwen-Image-2.1 on real weights, Studio: seam-free VAE tiles for Qwen-Image-2.1 on low-VRAM tiers (fixes thin horizontal / vertical lines) #12696's module against this one, at 1024x1024, 1344x1344, 2048x2048, 2752x1536 and 1184x864, with 0.6 to 8 GiB free:
    • without the fused kernels, tiled_decode is torch.equal wherever the floor tile fits;
    • with them, it is torch.equal at the floor tile;
    • the installed vae._encode is torch.equal in every case.

Not covered

  • Real 8 and 16 GB consumer cards were not run. The real-card evidence is an L4 and a T4 with VRAM ballast, and a G4 for HunyuanImage. The 8 / 12 / 16 GB end-to-end tiers are simulated on a B200.
  • HunyuanImage was not run on an L4 (the VM is short of host RAM) or a T4 (fp16, no GGUF). Qwen-Image FP8 did not fit an L4 VM's host RAM, so the GGUF ran instead.
  • Qwen-Image at 0.35 GiB free and 1664 to 2048 px still decodes 1.2 to 1.6x slower than pre-Studio: seam-free tiled decode for every image VAE whose stock tiles fall under the floor (HunyuanImage-2.1 grid, Qwen-Image, slivers) #12736 (decode alone, emulated). No Studio tier here got near that free memory.
  • The very-low-free cases are emulated: a filler tensor under the simulated cap, or a capped mem_get_info. On Windows (WDDM), an oversized tile spills to shared memory instead of raising an OOM, so only the floor-fit check protects there, not the OOM retry.
  • Part of the end-to-end run had the host over-subscribed. The interleaving puts that load on every arm alike, but SDXL's CPU-offload renders swung 1.3 to 37 s in every arm, so its totals say nothing. Its decode runs the same code as pre-Studio: seam-free tiled decode for every image VAE whose stock tiles fall under the floor (HunyuanImage-2.1 grid, Qwen-Image, slivers) #12736.
  • The cloud cells ran 00dbb42e78 (and FLUX.1 on an L4 at a4a70e3a74). The FLUX.1 T4 cell predates the last two commits, which only change the sliver path; with 8 GB free at decode time it decodes one tile on either.
  • The per-class figures were measured on a B200 with diffusers 0.41.0.dev0 and torch 2.12.1. Other GPUs and allocator settings, such as expandable_segments, may peak differently. The 25% headroom and the OOM retry cover that, but it was not measured.
  • FLUX.2 (and, less so, FLUX.1 and SDXL) shift colour per tile above 1024 px even where the stock tiles meet the floor: 27.3 dB at 2048 px for FLUX.2, a greener bottom-right tile. It is a per-tile offset, not a line, and those sizes keep the stock path here.
  • The boundary-gradient seam score does not separate stock from wide on these photos for any VAE, Qwen-Image-2.1 included. The PSNR, the worst 64 px window and the line score carry the result.
  • ComfyUI's own OOM fallback for the Qwen-Image VAE uses 32-latent tiles with 8-latent overlaps, as diffusers does. This PR follows its 16-latent 2D decode overlap instead, which measured slightly closer to untiled.
  • AMD, Windows and Apple Silicon were not run.

danielhanchen and others added 29 commits October 4, 2026 08:36
The Qwen-Image-2.1 VAE compresses 16x, but diffusers gives it the Wan VAE's
tile geometry in pixels (256 px tiles, 192 px stride). On this VAE that is a
16-latent tile with a 4-latent (64 px) blend, and the last row and column are
4-latent slivers. The decoder sees further than 4 latents past a tile edge,
so every seam and the right / bottom sliver decode differently from their
neighbours, and the short blend leaves thin vertical and horizontal lines
about one tile long. Every tier that tiles the decode (the 12 GB and 16 GB
streaming / offload plans) has drawn them since the family was added;
resident loads decode untiled and never showed them.

Decode this VAE in 32-latent tiles with at least a 16-latent overlap (the
overlap ComfyUI tiles its decode with), spread evenly so the last tile ends
at the image edge at full size. A tile gets no weight within 4 latents of an
edge it shares with another tile and ramps to full weight over the next 8,
normalised to 1 at every pixel, so the edge of an outer tile never shows
where three tiles overlap (strides under 16 latents). A canvas within one
tile decodes untiled. Tiles decode one at a time, so the decode peak does not
grow with the canvas.

Installed at load for every speed mode, before the fused batched tile decode,
so it covers the fused and stock paths. Only the decode changes: the tiled
encode and the tile attributes stay stock. vae_tile_side and the calibrated
tiled-decode activation report the 512 px tile. Kill switch
UNSLOTH_DIFFUSION_VAE_WIDE_TILES=0 (at load: skip the install; later: stock
tiled decode per call).

Tests cover the tile layout and blend weights at every size the Images page
offers, the per-call memory guard at those sizes, and a tiny random
AutoencoderKLQwenImage21 decoded tiled against untiled.
…w-VRAM tiers

The Qwen-Image-2.1 VAE compresses 16x, but diffusers gives it the Wan VAE's
tile geometry in pixels (256 px tiles, 192 px stride). On this VAE that is a
16-latent tile with a 4-latent (64 px) blend, and the last row and column are
4-latent slivers. The decoder sees further than 4 latents past a tile edge,
so every seam and the right / bottom sliver decode differently from their
neighbours, and the short blend leaves thin vertical and horizontal lines
about one tile long. Every tier that tiles the decode (the 12 GB and 16 GB
streaming / offload plans) has drawn them since the family was added;
resident loads decode untiled and never showed them.

Decode this VAE in 32-latent tiles with at least a 16-latent overlap (the
overlap ComfyUI tiles its decode with), spread evenly so the last tile ends
at the image edge at full size. A tile gets no weight within 4 latents of an
edge it shares with another tile and ramps to full weight over the next 8,
normalised to 1 at every pixel, so the edge of an outer tile never shows
where three tiles overlap (strides under 16 latents). A canvas within one
tile decodes untiled. Tiles decode one at a time, so the decode peak does not
grow with the canvas.

The stock tiled encode (img2img and edit inputs on the same tiers) has the
same geometry and seams; it gets the same tiles, blended in latent space.

Installed at load for every speed mode, before the fused batched tile decode,
so it covers the fused and stock paths. The VAE's tile attributes stay stock. vae_tile_side and the calibrated
tiled-decode activation report the 512 px tile. Kill switch
UNSLOTH_DIFFUSION_VAE_WIDE_TILES=0 (at load: skip the install; later: stock
tiled decode / encode per call).

Tests cover the tile layout and blend weights at every size the Images page
offers, the per-call memory guard at those sizes, and a tiny random
AutoencoderKLQwenImage21 decoded and encoded tiled against untiled.
The 32x32-latent decode tiles with 16-latent overlaps decode about 2.25x the
canvas at 1024x1024 against about 1.7x for the stock tiles, which costs about
0.25 s per decode on an L4. When half the free VRAM holds them, take larger
tiles, down to one untiled decode: fewer tiles decode less overlap, so they
are faster and leave fewer seams. The tile sides are the fewest that cover
each axis with 16-latent overlaps and fit the area budget (1.7 MiB of decode
peak per latent of tile area, measured on 32x32 to 64x64 tiles, plus the fp32
output accumulator). With no room for more, the decode keeps the 32x32 tiles
the planner budgets, so no plan or memory estimate changes.
UNSLOTH_DIFFUSION_VAE_MAX_TILE caps the tile side.
…AM tiers

The 12 / 16 GB tiers tile the VAE encode with diffusers' stock 16-latent tiles. The encoder
attends over each tile, so the condition latent shifts everywhere, not only at the seams, and
edits drift from the resident result. Encode in 64-latent tiles with 32-latent overlaps and the
decode's normalised ramp blend: a 1024 px reference (the default) is one tile and matches the
untiled encode exactly. Same kill switch as the decode.
# Conflicts:
#	studio/backend/core/inference/diffusion_vae_tiling.py
#	studio/backend/tests/test_diffusion_vae_tiling.py
# Conflicts:
#	studio/backend/core/inference/diffusion_vae_tiling.py
#	studio/backend/tests/test_diffusion_vae_tiling.py
This family only encodes edit inputs, at the reference resolution, and the per-call guard charges them as weighted condition pixels. Test that this covers the measured 64-latent encode tile (2,322 MiB unfused) at every reference resolution and output size, and describe the encode geometry in the module docstring.
…fall under the floor

Read the stock tile and overlap off each VAE (Wan-family pixel tiles and
strides, AutoencoderKL-style latent tiles and overlap factor) and its
compression ratio, instead of covering only AutoencoderKLQwenImage21.
Each tiled decode keeps diffusers' tiles when every tile is at least 32
latents and every overlap at least 16, and otherwise takes the wide,
edge-aligned tiles (floor 32 / 16, or the stock tile when larger).

This covers Qwen-Image / Qwen-Image-Edit / Krea-2 (8x, 8-latent blends),
HunyuanImage-2.1 (32x, 12-latent tiles with 3-latent blends) and the
edge-sliver sizes of AutoencoderKL / FLUX.2 (an 8-latent last tile at
1600 px), and keeps Qwen-Image-2.1 bit-identical. 4-D latents are
supported, the decode budget scales per output pixel, and KEEP_STOCK
holds VAEs that must keep stock tiles, with the measured reason.
…de more than stock at the floor

- Decode budget per VAE class from measured bf16 peaks (unfused / fused MiB per latent): Qwen-Image 0.27 / 0.18,
  Qwen-Image-2.1 1.7 / 0.43, HunyuanImage-2.1 1.3, AutoencoderKL and FLUX.2 0.17 / 0.09, the worst of each ratio
  for an unmeasured class. Tiles are sized on the fused figure where the fused VAE kernels are installed. The old
  per-pixel figure was 5x HunyuanImage's peak and 2.9x AutoencoderKL's.
- The floor layout of a VAE whose stock tile is larger than 32 latents picks the fewest tiles of the smallest side
  that keeps the overlap (two 116-latent tiles on a 1600 px FLUX.1 side, not two 128s), so it never decodes more
  than stock.
- A VAE whose stock tile is smaller than the floor tile (HunyuanImage, Qwen-Image-2.1) decodes in its stock tiles,
  with a logged reason, when even the floor tile cannot fit the free VRAM. An OOM in the wide tiles retries at the
  floor tile, then in the stock tiles.
- The stock tiles the wide path falls back to keep the fused batched tile loop on the Wan family.
- Geometry and blend weights are cached per VAE.
# Conflicts:
#	studio/backend/core/inference/diffusion.py
#	studio/backend/core/inference/diffusion_vae_tiling.py
# Conflicts:
#	studio/backend/core/inference/diffusion.py
#	studio/backend/core/inference/diffusion_vae_tiling.py
…es that decode no more than stock

The wide floor for a VAE whose stock tile is the floor tile (AutoencoderKL, FLUX.2, which take the wide path only
for an edge sliver) picked the smallest covering side (two 116-latent tiles on a 1600 px side). That kept the seam
lines away but lost per-tile context against the 128-latent tiles: SDXL's worst 64 px window rose from 3.1 to 5.1
levels on DIV2K 0010. Now it takes the fewest decoder calls, then the largest tile, among layouts that decode no
more latents per side than the stock loop: two 120-latent tiles (240 decoded latents, as stock, in 4 calls instead
of 9). Qwen-Image, Qwen-Image-2.1 and HunyuanImage keep the fewest-latents choice.
…to device-free memory

A VAE whose stock tile is the floor tile takes the wide tiles only to drop an edge sliver. Counting the allocator's
cached blocks as free let FLUX.1 at 1600 px on an 8 GB tier decode untiled (3.7 GB) out of the denoiser's cache;
3 of 5 decodes stalled on cache flushes (0.21 s median against stock's 0.08). Its tiles now grow past the floor only
into the memory the device has free. Qwen-Image, Qwen-Image-2.1 and HunyuanImage still count the cache, as #12696
does (measured faster there), and the floor-fit check counts it for every VAE.
…iles-general

# Conflicts:
#	studio/backend/core/inference/diffusion.py
#	studio/backend/core/inference/diffusion_vae_tiling.py
#	studio/backend/tests/test_diffusion_vae_tiling.py
…l' into studio/vae-seamfree-tiles-general

# Conflicts:
#	studio/backend/core/inference/diffusion.py
#	studio/backend/core/inference/diffusion_vae_tiling.py
#	studio/backend/tests/test_diffusion_vae_tiling_general.py
# Conflicts:
#	studio/backend/core/inference/diffusion_vae_tiling.py
#	studio/backend/tests/test_diffusion_vae_tiling_general.py
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-06T03:57:31.608370Z dd41323 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Oct 6, 2026
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 2d8ada8744

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant