Repository navigation
Studio: size seam-free VAE tiles from measured peaks (no slower than stock, no low-VRAM OOM) - #12764
Merged
Merged
Conversation
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.
…d seam-free tiled encode
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
…studio/vae-tile-seams-followup
…studio/q21-line-artifacts
# 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
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Member
Author
|
@codex review |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
1 of 5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Change
diffusion_vae_tiling.py(and the fused VAE module's batched loop, so the fallback keeps it):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.Performance
Arms: pre-#12736 is main before #12736 (
df4eb29579). #12696 is27916a55fb. #12736 isacc589fbe5, 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_infois 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.torch.equalto Studio: seam-free VAE tiles for Qwen-Image-2.1 on low-VRAM tiers (fixes thin horizontal / vertical lines) #12696. With 1 GiB or more free it takes larger tiles: 1.3 to 2.2x faster than Studio: seam-free VAE tiles for Qwen-Image-2.1 on low-VRAM tiers (fixes thin horizontal / vertical lines) #12696, and closer to untiled (53 to 61 dB against 50 to 54). At 0.35 GiB free the floor tile (about 0.45 GiB with the fused kernels) cannot fit, so it decodes in the stock tiles. Studio: seam-free VAE tiles for Qwen-Image-2.1 on low-VRAM tiers (fixes thin horizontal / vertical lines) #12696 OOMs there (see the low-free table below).End to end through Studio's DiffusionBackend
In-process
DiffusionBackend.load_pipeline/generateon an exclusive B200, withtransformer_quant=autoand every other setting at its default. Studio's 16, 12 and 8 GB tiers are simulated:mem_get_inforeports that total, and the allocator is capped withset_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 istorch.cuda.max_memory_allocatedover 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.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:
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.a4a70e3a7400dbb42e78. The two later commits change only the AutoencoderKL / FLUX.2 sliver path, so the Qwen-Image-2.1, Qwen-Image and HunyuanImage cells are unchanged ata4a70e3a74. The L4 FLUX.1 cell was rerun ona4a70e3a74(last row). Once model CPU offload has parked the denoiser, the device has room for one untiled 1600 px decode there, so it stays untiled, as at00dbb42e78. On the T4, with 8 GB free at decode time, FLUX.1's 1600 px decode is one tile on either head.Tests
tests/test_diffusion_vae_tiling_general.py(2,388 tests, CPU, tiny diffusers VAEs of each class):KEEP_STOCK, and an unreadable geometry keeping stock;vaeannotation) is one the rule reads, or is listed inKEEP_STOCK.tiled_decodeback on uninstall;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.tiled_decodeistorch.equalwherever the floor tile fits;torch.equalat the floor tile;vae._encodeistorch.equalin every case.Not covered
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.00dbb42e78(and FLUX.1 on an L4 ata4a70e3a74). 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.expandable_segments, may peak differently. The 25% headroom and the OOM retry cover that, but it was not measured.