You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Browse filesBrowse the repository at this point in the historyBrowse files
authored
perf(qwen-image): add a tiling option to the Qwen-Image VAE nodes (#9427)
* feat(qwen-image): add a tiling option to the image-to-latents node
The Qwen-Image i2l node hardcoded vae.disable_tiling(), so a full-frame encode
was the only option. At 2560x1440 that peaks at 9.26 GiB — on top of a resident
multi-GB transformer, which is what makes an upscale round-trip run out of
headroom exactly at this node while every other node fits.
Adds `tiled` / `tile_size` input fields following the SD/SDXL i2l node, OR'd
with the global force_tiled_decode setting. Off by default, so behaviour is
unchanged unless enabled.
estimate_vae_working_memory_qwen_image gains a matching tile_size parameter.
Without it the change would be inert: the cache would keep reserving the
full-frame figure (10.99 GiB at 2560x1440) and evict models to honour it, no
matter what the VAE actually does. Tiled, it budgets one tile plus 25% overlap
plus the resident RGB image, mirroring estimate_vae_working_memory_wan.
Measured through the node at 2560x1440: 10.99 -> 0.26 GiB reserved, 9.26 -> 0.17
GiB actual peak, identical latent shape. Tiled latents differ by ~1.4% relative
L2 on noise input (worst case for tile blending; real images blend far better),
which is why this stays opt-in.
* Add test
* feat(qwen-image): make VAE tiling usable on both Qwen-Image VAE nodes
Both nodes reserve working memory for a full-frame operation, which at high
resolutions exceeds a 24 GB card, so the model cache evicts everything else to
honour it. On CUDA at 2560x1440: 19.91 GiB for the decode and 10.99 GiB for the
encode.
Tiling is the intended escape hatch, but it did not work on either node:
- qwen_image_i2l hardcoded vae.disable_tiling(), so it could not be enabled.
- qwen_image_l2i honoured the global force_tiled_decode, but computed its
working-memory estimate before and independently of that flag. Tiling bounded
the VAE while the cache still reserved the full-frame figure, so the memory was
never freed for anything else — effectively inert.
Adds `tiled` / `tile_size` input fields to both nodes following the SD/SDXL
i2l/l2i nodes, OR'd with force_tiled_decode. Off by default; behaviour is
unchanged unless enabled.
estimate_vae_working_memory_qwen_image gains a matching tile_size parameter, and
both nodes resolve tile_size=0 to the VAE default (256px) before estimating.
Tiled it budgets one tile plus 25% overlap plus the resident RGB image,
mirroring estimate_vae_working_memory_wan. Without this the change would be
cosmetic on i2l and remain inert on l2i.
Measured through the i2l node at 2560x1440: 10.99 -> 0.26 GiB reserved,
9.26 -> 0.17 GiB actual peak, identical latent shape. Verified across eight
resolutions that tiled and untiled encodes produce the same latent dimensions.
Tiled latents differ by ~1.4% relative L2 on noise input (worst case for tile
blending), which is why this stays opt-in.
Also fixes a crash in qwen_image_i2l: `width`/`height` are `int | None`, but the
workflow UI sends 0 for an unset number input, and `0 is not None` reached
`image.resize((0, 0))` -> "height and width must be > 0". Non-positive values are
now treated as unset, matching how tile_size uses 0.
* fix(qwen-image): pass a matched tile stride and scope the tiling state
enable_tiling() was called with tile_sample_min_* only, leaving the stride at
the module's 192px default. The tile loops step by stride but slice each
accumulated tile to min, so any tile_size below 192 silently dropped whole
bands of the image -- a 128px tile turned a 512x512 decode into 384x384 with
no error -- while sizes above 256 grew every tile without removing any,
making compute scale with tile_size^2 (8x a full frame at 512px).
Pass all four parameters with the stock 4:3 ratio, rounding the stride down to
a multiple of the 8x spatial compression so the pixel and latent steps agree.
Tile sizes below 64px are clamped; the field carries the 0 "use default"
sentinel and so cannot take a pydantic lower bound.
enable_tiling() also writes straight onto the module, and disable_tiling()
only clears use_tiling. That module is the model cache's own instance, so a
tile size set once persisted for the lifetime of the cache entry and leaked
across invocations and into anima_latents_to_image, which shares the instance.
Apply the geometry through a context manager that restores it, and resolve the
0 sentinel against a constant instead of the module's current value.
Also budget the pixel-space buffers tiled_decode holds simultaneously (~5
frames, not 1) -- the term that grows with output area, so it degraded in
exactly the regime tiling exists for.
* test(qwen-image): pin the multiple-of-8 tile-stride rounding
The rounding in `_tile_stride_for` was load-bearing but uncovered: dropping it
left the whole suite green, yet a raw 3/4 stride silently truncates the encode
for any tile size whose 3/4 is not a multiple of 8. `tile_size` is
`multiple_of=8`, so 72 and 80 are both reachable from the workflow UI and both
land there (54 and 60).
Cover it at the argument level (72 -> 48, 80 -> 56) and end-to-end against a
real tiny VAE, plus a pin on the failure mode itself: min=72 with the
un-rounded stride 54 turns a 512x512 encode into a 57x57 latent instead of
64x64, with no exception.
Also correct the rationale on QWEN_IMAGE_VAE_MIN_TILE_SIZE. `_tile_stride_for`
already floors the stride at 8, so the derived latent step never collapses to
0, and 8/24/32 all round to clean multiples of 8 and produce correctly sized
output -- 64 is not the smallest valid tile. It is a cost floor: the tile count
grows with the inverse square of the stride (1620 tiles at 64px versus 57,600
at 8px on a 2560x1440 frame).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
0 commit comments