Skip to content

DistVAE updates including tile-parallel VAE execution - #750

Merged
avjves merged 83 commits into
xdit-project:mainfrom
pds-amd:feat/vae-tile-batching
Aug 21, 2026
Merged

DistVAE updates including tile-parallel VAE execution#750
avjves merged 83 commits into
xdit-project:mainfrom
pds-amd:feat/vae-tile-batching

Conversation

@pds-amd

@pds-amd pds-amd commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

When VAE tiling and --use_parallel_vae are active on a supported VAE, DistVAE assigns complete tiles to ranks and assembles the decoded output afterward. Without tile distribution, the decoder is sharded along its latent-height axis.

VAE tiling primarily reduces peak memory. Complete-tile distribution can also reduce decode latency when the tile grid supplies enough balanced work.

Tiling also has a nice side effect of shorter compile times.

Some illustrative benchmark examples are provided below.

Execution modes

Configuration Decoder execution
--use_parallel_vae Row-sharded decode
--enable_tiling Local tiled decode
Both flags, supported VAE Complete tiles distributed across VAE ranks
Exact tile size or overlap flags Enables tiling and uses the requested output-pixel geometry

Models declaring use_parallel_vae_encoder=True also receive a row-sharded encoder. Complete-tile distribution applies only to decoding.

Row sharding splits one latent along its height axis. It can reject shapes that do not align with the VAE's patch or downsampling unit, leave too few bands for the selected rank count, or cannot provide the required convolution halo. Complete-tile distribution avoids those intra-tile split constraints and can therefore use configurations that row sharding rejects.

Included

  • Add VAEManager to configure every unique VAE used by single-stage and multi-stage pipelines.
  • Add exact output-pixel tile controls:
    • --vae_tile_size_height
    • --vae_tile_size_width
    • --vae_tile_overlap_height
    • --vae_tile_overlap_width
  • Preserve rectangular tile geometry and validate it against the selected execution mode.
  • Support in-process GroupCoordinator groups and dedicated raw ProcessGroup instances.
  • Apply channels-last conversion and VAE options once per VAE and in lifecycle order.
  • Restore Torch GroupNorm before DistVAE adapter discovery when AITER replaced it.
  • Report actionable shape, padding, and OOM diagnostics. Decoder row-split failures suggest complete-tile decoding when supported.
  • Require the DistVAE beta9 public API instead of importing adapter implementations directly.

Dependency

Depends on the companion DistVAE beta9 change. xdit-project/DistVAE#14

DistVAE beta9 must be published before this PR merges. The latest version currently available from PyPI is beta5, while setup.py requires distvae>=0.0.0beta9.

Benchmark results

gfx1201

End-to-end latency and peak VRAM by VAE decode path.
gfx1201 (AMD Radeon AI Pro R9700S, 32 GiB/card).

model shape GPUs steps no sharding row sharding example tiled plan
FLUX.2-klein-4B 1024x1024 2 4 1.24 s · 19,824 MB 1.19 s · 16,913 MB 1.18 s · 14,498 MB
192x1024 ov 64x0
FLUX.2-klein-4B 1024x1024 4 4 0.97 s · 20,336 MB 0.85 s · 15,690 MB 0.82 s · 14,531 MB
128x1024 ov 32x0
FLUX.2-klein-4B 2048x2048 2 4 OOM / failed 5.42 s · 27,495 MB 4.99 s · 18,236 MB
384x2048 ov 128x0
FLUX.2-klein-4B 2048x2048 4 4 OOM / failed 3.57 s · 21,259 MB 2.98 s · 16,246 MB
192x2048 ov 64x0
Qwen-Image-2512 1024x1024 2 50 67.23 s · 29,056 MB 66.87 s · 29,233 MB 67.10 s · 28,609 MB
192x1024 ov 64x0
Qwen-Image-2512 1024x1024 4 50 94.99 s · 21,125 MB 94.54 s · 20,867 MB 94.39 s · 20,719 MB
384x1024 ov 128x0
Qwen-Image-2512 2048x2048 2 50 not run OOM / failed 201.54 s · 28,730 MB
384x2048 ov 128x0
Qwen-Image-2512 2048x2048 4 50 OOM / failed 165.28 s · 22,693 MB 164.55 s · 20,752 MB
192x2048 ov 64x0
Wan2.2-TI2V-5B 1280x736x81f 2 50 OOM / failed OOM / failed 190.36 s · 30,859 MB
384x1152 ov 96x0
Wan2.2-TI2V-5B 1280x736x81f 4 50 OOM / failed 110.51 s · 28,873 MB 109.07 s · 25,161 MB
192x1152 ov 48x0

Wan2.2-TI2V-5B Tiled

wan-4gpu

Row Sharded

row-wan.81f.4g.row.mp4

Tiled

tiled-wan.81f.4g.fine.mp4

Qwen-Image-2512 2K

qwen-2048-4gpu
None Row Sharded Tiled
OOM row-qwen 2k 4g row tiled-qwen 2k 4g fine

FLUX.2-klein-4B 1k

flux2-1024-4gpu
  • Note this example has a very fine tile configured (128px strips overlapping by 32px) so banding starts to appear highlighting that with tiling you still need to tune the tile size to avoid these artefacts.
None Row Sharded Tiled
none-flux2k 1k 4g unsharded row-flux2k 1k 4g row tiled-flux2k 1k 4g fine

Held fixed across every run: attention_backend=aiter_flydsl_fp8, ring_degree=1, seed=42,
use_fp8_gemms=true, use_torch_compile=true, warmup_calls=1.
ulysses_degree is set to the GPU count, 2 or 4.
fully_shard_degree is set to the GPU count, 2 or 4 (Qwen-Image-2512 only).
FLUX.2-klein-4B only: guidance_scale=1.0, num_iterations=2, prompt=A ginger kitten.
Qwen-Image-2512 only: guidance_scale=0.0, memory_efficient_sharding=true, num_iterations=1.
Wan2.2-TI2V-5B only: flow_shift=5, guidance_scale=5.0, task=i2v,
input_images=/app/data/wan_input.jpg, num_iterations=1.

gfx950

End-to-end latency by VAE decode path.
gfx950 (AMD Instinct MI355X, 287.98 GiB/card),

model shape GPUs steps no sharding row sharding example tiled plan
FLUX.1-dev 2048x2048 8 25 2.14 s 2.07 s 2.07 s 128x2048 ov 32x0
FLUX.2-dev 2048x2048 8 50 9.29 s 9.21 s 9.22 s 128x2048 ov 32x0
Qwen-Image-2512 2048x2048 8 50 4.63 s 4.43 s 4.41 s 192x2048 ov 64x0
LTX-2.3 1088x1920x121f 8 30 15.12 s refused* 14.32 s native window
Wan2.2-I2V-A14B 1104x832x81f 8 40 73.18 s 69.94 s 71.35 s 192x832 ov 48x0
HunyuanVideo 720x1280x129f 8 50 OOM (asks 201.71 GiB) 85.71 s 85.94 s 736x224 ov 0x64
HunyuanVideo-1.5-720p 720x1280x129f 8 50 OOM (asks 191.33 GiB) 98.50 s 97.79 s 256x256 ov 64x64
  • *LTX-2 has no row-sharded arm at this input image geometry - highlighting that tiled parallelism can address such cases.

HunyuanVideo-1.5-720p

vram-timeline

Row Sharded

hunyuan_video_1_5_u8r1_tc_True_720x1280_t2v_0.-.row.mp4

Tiled

hunyuan_video_1_5_u8r1_tc_True_720x1280_t2v_0.mp4

Held fixed across every run: attention_backend=aiter, ulysses_degree=8, ring_degree=1,
use_torch_compile=true, use_fp8_gemms=false, num_iterations=1, warmup_calls=1. Everything
else is that model's CI config unchanged, and the per-model settings that differ are:
FLUX.1-dev — seed=0, guidance_scale=0.0, max_sequence_length=256.
FLUX.2-dev — seed=42, guidance_scale=4.0, max_sequence_length=512.
Qwen-Image-2512 — seed=42, guidance_scale=0.0.
LTX-2.3 — seed=42, guidance_scale=3.0; the pipeline fills in its own frame count, 121.
Wan2.2-I2V-A14B — seed=42, input_images=/app/data/wan_input.jpg, num_frames=81; it is
image-to-video and resizes its canvas to the input, so it decodes 1104×832 rather than the
720×1280 in the config.
HunyuanVideo — seed=1168860793, guidance_scale=6.0, batch_size=1.
HunyuanVideo-1.5-720p — seed=1168860793, task=t2v.

Additional fixes

  • Treat boolean model capabilities as flags rather than integer parallel degrees during validation.
  • Apply VAE setup, channels-last conversion, slicing, and tiling to every VAE in staged pipelines.
  • Replace model-specific DistVAE adapter construction in Cosmos3, Wan, FLUX, Ideogram, and related runners with the public adapter registry.
  • Enable supported VAE decoder or encoder parallelism for additional model runners, including LTX-2, Krea2, Stable Diffusion, Z-Image, VACE, and editing pipelines.
  • Move Ideogram wrapper imports into load paths so optional Diffusers symbols do not break module discovery.
  • Remove the optional-DistVAE environment probe and pin the DistVAE dependency.

Test maintenance

  • Make device-selection tests independent of whether PyTorch was built for CUDA or ROCm.
  • Skip flash-attention test modules when the optional flash-attn package is unavailable.
  • Update FSDP sharding tests to exercise the current shard_component API.
  • Correct the Gilbert-neighbor tests: a line of blocks produces a tridiagonal adjacency mask, while the original 2×2×2 fixture is fully connected.
  • Add lifecycle coverage for staged VAEs and parallel-setup ordering.

pds-amd and others added 30 commits August 4, 2026 10:50
…ty rewrites

ModelCapabilities is frozen, so re-declaring the whole block without a flag
reads as False rather than leaving it alone. Wan2.2 distilled I2V, Qwen-Image
and Z-Image-Turbo each lost enable_tiling and enable_slicing that way, which
silently refused --enable_tiling on three models that support it.
CausalWan sets use_fp8_gemms=False in the same class body that lists
fp8_gemm_module_list=["transformer.blocks"], so --use_fp8_gemms quantizes
the transformer blocks today. The declaration is what is wrong: nothing
else in the model refuses FP8, and the capability check that would have
caught the contradiction never ran for boolean flags.
The capability loop tested every int-valued config with value > 1, written
for degrees like ulysses_degree=4. bool subclasses int, so True took that
branch, True > 1 is False, and no boolean capability was ever refused:
--use_parallel_vae on a model that cannot do it ran as a silent no-op
rather than an error, and the same held for every other boolean flag.

Booleans now take the branch already written for them. The declarations
this makes load-bearing were audited against the code that reads them:
every model declares the VAE flags, use_vae_channels_last_format defaults
true, the quantization flags agree with each model's gemm module lists
after the CausalWan fix, and both hybrid schedule flags are declared
exactly where _calculate_hybrid_attention_step_multiplier accounts for
the model's steps.
One of the two definitions described tiling, a tile at a time, which is
what --enable_tiling does. Slicing decodes one batch item at a time and
does nothing at batch size 1.
…not honour

Diffusers hands every autoencoder enable_tiling and enable_slicing through
a shared mixin whether or not the class implements them, so calling one on
a VAE too old to tile did nothing and reported nothing. Both features also
arrived class by class over several releases, Wan's tiling in 0.34, one
past the diffusers floor setup.py asks for, so an install that satisfies
the floor can still be short of the class the model loads.

Probing the state flag the mixin itself checks, rather than the method,
answers whether the installed release can really do it. That knowledge
lives in a new xfuser/core/utils/vae_tiling.py so nothing about diffusers
VAE internals accumulates in the runner.
… through

A staged model loads a second pipeline with its own VAE, and the later
stage is the one decoding at full resolution. --enable_tiling only reached
self.pipe.vae, so LTX-2 carried its own _enable_options override to pass
the flags along to the second stage, and LTX-2.3's override handled
slicing only.

Collecting the VAEs in the base class covers both stages for every staged
model, including ones added later, and both overrides come out. The
alternative, a hook each subclass overrides, fails silently: a subclass
that forgets gets no error, only a flag that misses the largest decode.
A VAE's default tile window tracks its training resolution and never
shrinks, so decoding above that resolution can run out of memory with
tiling already on: the tile itself is too large. The window was only
reachable by setting diffusers attributes by hand, and which attributes
depends on the VAE class.

--vae_tile_size takes one pixel-space edge and rescales whichever
attributes the class carries, covering all four families diffusers ships:
the legacy latent/sample pair, the stride form, per-axis overlap factors,
and overlap-only. Sizes the attributes cannot express exactly snap down to
the nearest workable window rather than rounding into a fractional latent
size the decoder would fail on, and a size above the VAE's own window is
declined, since widening it would raise peak memory.

The flag turns tiling on by itself. LTX-2.3 tiles its stage-2 VAE at load,
so requiring --enable_tiling alongside would mean tiling every stage to
reach the one that ran out of memory.

The arithmetic and the diffusers attribute knowledge live in vae_tiling,
tested against every VAE class the runner models load.
A VAE decode that ran out of memory reported only torch's allocation
figures, from a stage most of the memory-saving flags do not name, so the
next step was not obvious. The guard adds the applicable one: tiling where
the VAE is not tiling yet, otherwise a concrete --vae_tile_size below the
current window, chosen through the same snapping the flag itself uses so
the suggestion cannot be refused on the next run. It reads the VAE's own
state rather than the config flags, since a model can arrive already
tiling with no flag set.

A narrowed window can also leave a tile thinner than a convolution's
padding, which torch reports from deep inside the decoder without
mentioning tiles. That message is kept and prefaced with the window that
caused it. Only failures matching that padding signature are reattributed,
and only when this run narrowed the window, so a dtype or device error
still surfaces as itself.
Five runner files each carried their own module-level _setup_parallel_vae,
differing only in what the VAE they were written for needed. A sixth model
wanting parallel VAE meant copying a fifth wiring, and knowing which of
DistVAE's adapters its decoder blocks call for by reading someone else's.

The VAE answers that itself: its decoder blocks say which adapter fits,
and refusing up front replaces an assertion thrown from inside a
half-built replacement decoder with an error naming the flag and pointing
at --vae_tile_size. So the base class can do the whole job, and every
call site becomes the same three lines.

What looked model-specific was a function of the VAE's config. Both quirks
come from patch_size: the encoder adapter shards by the encoder's own
downsampling, so a patching VAE has to divide that factor out, and the
decoder adapter's patchify assumes no patching because Wan does none.
Deriving both reproduces every value the five copies passed, Cosmos 3's
16 over 2 included, and is a no-op for VAEs that do not patch.

A missing DistVAE adapter now raises instead of logging and running
single-rank. Asking for a sharded encoder and silently getting only a
sharded decoder is the kind of quiet no-op this branch removes elsewhere.
…e VAE

Z-Image, Z-Image-Turbo and SD3.5 decode through AutoencoderKL variants
built from the 2D blocks DecoderAdapter was written against, and CausalWan
and Wan2.1-VACE through Wan's blocks, so all five can shard a decode they
were quietly ignoring the flag for.

Nine models still cannot: Qwen-Image and Krea-2, the HunyuanVideo family
and LTX-2, each of whose decoders is built from blocks DistVAE has no
adapter for. They fail naming the missing adapter and pointing at
--vae_tile_size, which lowers the same memory without a second GPU.
Sharing the decoder wiring meant Cosmos 3's patch_size read now runs for
every VAE, and patch_size does not mean the same thing in every VAE.
Flux 2 declares (2, 2) for the pixel unshuffle at its boundary, so
`patch_size > 1` raised TypeError comparing a tuple to an int, and
--use_parallel_vae broke on Flux 2 and Ideogram 4, which wrapped their
decoders without reading any config before. Anything other than a single
number now reads as no patching, which is what it is to an adapter.

The tests only asked which adapter fits each VAE class, never wrapped
with it, because the real adapters need a process group. Standing one in
covers the wrapping too, so every VAE class a runner loads is now put
through the config reads that wrapping does, Flux 2's patch_size
included.
…w fits

DistVAE has adapters for the QwenImage, HunyuanVideo, HunyuanVideo 1.5 and LTX-2 decoders, so
Qwen-Image, Qwen-Image-Edit, Krea-2-Raw, Krea-2-Turbo, HunyuanVideo, HunyuanVideo 1.5 and its
distilled and sparse variants, LTX-2 and LTX-2.3 can shard a decode they were being refused.
That is every VAE class a runner model loads, which is what the expectations here now say.

Picking an adapter was a special case for Wan wrapped around the 2D one; the four new families
would have made four more of it, so the block types each family's decoder is built from move
into a table. Their names are read off the installed diffusers rather than imported, the way
Wan's already were, so a VAE newer than the install matches nothing instead of breaking the
choice for every other VAE.

The one shape DistVAE cannot take is an LTX-2 decoder with inject_noise on, where each rank
would draw noise for its own rows and the ranks together would not reconstruct what one draws.
It is refused here rather than by the adapter, which discovers it partway through replacing a
decoder and would leave the VAE half sharded. No released LTX-2 checkpoint enables it.

Co-authored-by: Cursor <cursoragent@cursor.com>
parallelize_encoder read the decoder to decide what the encoder was, which worked only while
DistVAE had a single encoder adapter and the decoder was still recognisable. The runner shards the
decoder first, and that replaces its up blocks with adapters, so by the time the encoder is asked
for the decoder answers to nothing and every model declaring the flag was refused. DistVAE now has
an encoder adapter for all seven families, so the table gains a column for them and each half is
recognised by its own blocks, which is the only way either can be read after the other is sharded.

Wan needs two shapes in that column: 2.2 groups each encoder stage into a WanResidualDownBlock,
while 2.1 lays the same residual blocks, attentions and resamples out flat. A 2D encoder is
matched on DownEncoderBlock2D as before, and its ratio is counted off the stages that downsample
rather than defaulting to 8, because these VAEs record no ratio anywhere and the adapter cuts
bands by it: a three-stage checkpoint narrows by 4 and would otherwise be cut for 8.

Co-authored-by: Cursor <cursoragent@cursor.com>
…code

Thirteen models VAE-encode something before they generate, and every one of them was paying for
that on a single rank while its decode was sharded. Ten classes declare it here; Flux.2-Klein-4B,
HunyuanVideo-1.5-Distilled and LingBot-Video-Dense inherit the block from a parent that does.

What is encoded, and how large, differs by model. LingBot's refiner is the extreme: it encodes the
whole base video at the final output resolution, up to 1080p, which is the largest encode of the
set. Wan2.1-VACE is next, encoding a full-length conditioning video twice over, once for each half
of the mask, plus a pass per reference image. Flux-Kontext, Qwen-Image-Edit and FLUX.2 encode a
single image at generation size, and HunyuanVideo 1.5 and CausalWan a single frame, where the win
is smaller but the adapter is free.

Five of the ten only encode on some runs. Wan2.2-TI2V and HunyuanVideo 1.5 load a pipeline that
encodes for --task i2v and one that does not for t2v; FLUX.2, Klein and CausalWan encode the
reference images they are given and nothing when given none. A sharded encoder that never runs
costs nothing, so the flag is declared once rather than per task.

Sizing is the one thing this relies on: the adapter cuts an image into bands that are whole
multiples of the VAE's ratio, and each of these pipelines already holds what it encodes to a
multiple of that ratio, at the size it is about to generate at.

Co-authored-by: Cursor <cursoragent@cursor.com>
Every decoder call costs the same whatever the tile it is given, and under
--use_parallel_vae that cost is a round of collectives rather than arithmetic: a
Patchify, a halo exchange per convolution, a reduction per norm, and a
DePatchify to gather the result. Measured on FLUX.2-Klein-4B at 4 GPU it is
~26 ms, flat from 121 tiles to 1849. Narrowing the window to fit a decode in
memory therefore multiplies a fixed cost, which is what leaves a 64px window an
order of magnitude slower than the VAE's own.

Tiles are independent and, away from the edges the latent bounds clip, all the
same shape, so they can be stacked on the batch dimension and share that round.
The budget is an area rather than a count, since activation memory follows the
area being decoded and one count would batch the widest tiles into an
allocation the VAE was never sized for. The area used is the VAE's own tile,
read before --vae_tile_size can narrow it, so a run that did not ask for a
smaller tile decodes exactly as it did and a run that did stops paying for its
size twice. Nothing to set, and no new flag.

Scoped to the VAEs that tile by overlap fraction, AutoencoderKL and
AutoencoderKLFlux2. Wan, Qwen-Image and the video VAEs walk a stride they store
outright over a loop with its own blending, and keep their own tiled_decode.
…ds it

Reverting AITER's swap of torch.nn.GroupNorm sat in the branch that validates
--use_parallel_vae, next to the check that DistVAE is installed, where it read
as one more thing the flag disallows. It is not: it is a condition the sharding
itself has, and the reason is entirely about how the adapters find their norms.
Moving it next to the isinstance check it protects puts the reason where the
next person to touch that check will see it.

Behaviour is unchanged. The three tests are new; nothing exercised this before,
including the failure it exists for, which is a VAE reading as unshardable
because its norms are not the class the check asks about.
…om it

Qwen-Image sizes its causal feature cache by counting its causal convolutions
on every call, asking isinstance against its own class. Sharding replaces all of
them, so the count came to zero, the list was empty, and the first convolution
to want its entry indexed off the end: an IndexError from inside diffusers on
the first decode. Counting before either half is replaced and holding the answer
is what the VAE would have done had it cached the count, as Wan does.
test_sharding.py imported shard_transformer_blocks, which exists neither here
nor on upstream main - it became shard_component, taking a list of attributes to
wrap rather than one attribute name. The import error failed collection for the
whole of tests/core, so no xDiT test could be run by directory. The invalid-attr
case now expects the AttributeError rgetattr raises rather than the ValueError
the older function checked for.
A ROCm install has neither, and an import error at module scope stops the whole
of tests/core collecting rather than just these two.
test_vae_parallel takes its VAE configs from test_vae_tiling rather than
repeating them, which needs the repo root on sys.path and tests to be the one
here. Some installs carry an unrelated top-level tests package, and a namespace
directory loses to an installed package wherever it sits on the path, so this
takes the __init__.py that makes it a real one as well.
On ROCm, importing xfuser swaps torch.nn.GroupNorm for AITER's, so these built
VAEs whose norms DistVAE will not shard, and decoding one on the CPU reaches a
kernel that only exists on the GPU. Production reverts before building a VAE it
means to shard; a session fixture does the same for the tests, and the revert
tests name the class they expect rather than whichever was left behind.
…rest do

The Ideogram 4 and LingBot Video runners pulled their transformer wrapper in at
module scope, so importing the registry paid for both. tests/core/test_compat
has been asking for this since it landed.
It reads torch.version.cuda and torch.version.hip, never torch.cuda.is_available,
so patching availability only ever agreed with the machine it ran on: a ROCm
build answers to HIP and reported cuda where the test asked for cpu.
The two mask tests asked for a strictly diagonal answer from a 2x2x2 cube, in
which every point is within one step of every other and every block therefore
neighbours every block. They now run along a row, where a block touches itself
and the two beside it, and the cube keeps its own test saying what it is.
Measured on FLUX.2-Klein-4B at 2048x2048 over 4 GPUs: peak VRAM is 21.9 GB
untiled and 15.9 GB at the VAE's own window, then flat - 15.5 at 512px, 15.7 at
256 and 128, and back up to 16.4 at 64 as per-tile bookkeeping outweighs the
smaller working set. Over that same range the share of pixels more than 10% off
the untiled decode goes 1.5%, 3.9%, 11.0%, 13.6%, 14.6%, since each tile is
normalized over its own content and the blend that hides the seams is a fixed
fraction of a shrinking window. Neither sharding nor tile batching contributes:
plain tiling, tiling with --use_parallel_vae, and both with batching agree to
within 0.05 points at every window.

So the knob is not one to keep turning. Once the VAE is no longer what peaks,
shrinking it cannot lower the peak, and everything past that point is image
quality spent for nothing. Both help strings and the OOM hint now say to step
once and compare peak VRAM. No behaviour change.
The batch budget was the VAE's own tile area, held constant however narrow the
window got. That made every decoder call carry the same area as the default
window, which is the whole of what --vae_tile_size is asked to reduce: measured
on Klein-4B at 2048x2048, a 64px window peaked at 16354 MB batched against
15106 MB unbatched, and 16213 MB at the default window. Someone narrowing the
window to fit a decode was spending more memory than if they had left the flag
alone, and taking the image degradation of a small tile for it.

Budget by the geometric mean of the VAE's own tile area and the narrowed one
instead. Halving the window's area now halves the area a call carries and
doubles the tiles sharing each round of collectives, so both of the things the
knob is reached for keep improving as it shrinks. At the VAE's own window the
two areas are equal, the budget is one tile, and the decode is bit-identical to
upstream as before.

It is a trade, not a free win: a 64px window batches 16 tiles a call rather than
256, so its collective count rises with it. That is the right end to give ground
on, since a window that small degrades the image badly enough that no one should
be sitting at one for long.
The rule in 9feba6d is right but its explanation was written in tile areas,
which is not what anyone sets, and the commit message got the scaling wrong as
a result. The budget is the geometric mean of the two areas, which is the
product of the two edges, so it is linear in the edge --vae_tile_size names:
halve the number and the area a decoder call carries halves. Equivalently, a
batched decode at window W costs what an unbatched decode at the geometric mean
of W and the VAE's own window costs.

Same arithmetic, no behaviour change. The test now asserts the linear-in-edge
property directly rather than a table of areas that happened to satisfy it.
…shard each one

Tiling and --use_parallel_vae divide the same decode, and composing them divided it twice.
DistVAE shards the rows of whatever it is handed and a tiled decode hands it one tile at a
time, so a Patchify, a halo exchange per convolution, a reduction per norm and a gather were
paid per tile rather than per decode. On a Wan decode at 768x768x49f the collectives came to
exactly sixteen times the untiled count, one multiple per tile, and four ranks decoded no
faster than one: each rank's share of the arithmetic shrank while its share of the exchanges
did not.

Tiles are independent, which the rows inside a tile are not. Where the tiling loop is one
xFuser owns, the decoder is now left as it is and whole tiles go out to the ranks instead,
which costs two exchanges for the whole decode however many tiles there are. A tile narrower
than the group also stops being a refusal, since nothing divides a tile any more.
Wan and Qwen-Image walk a stride they store, over a loop of their own with a frame axis and a
feature cache, so the batched decode left them alone and their tiles were the ones paying the
per-tile collective bill most heavily: sixteen tiles at 768x768x49f, sixteen times the
exchanges, and four ranks no faster than one.

The cache is threaded through the frames of a tile and cleared where each tile starts, so a
tile is independent of every other tile in the way its frames are not, and the loop is
reimplemented here only to hand those tiles round a group. Without a group to hand them to the
VAE keeps its own loop, since there would be nothing to gain from a second author. The two
classes are recognised by name, because nothing about a VAE's attributes says which loop body
it has: Hunyuan, LTX-2 and CogVideoX carry the same stride attributes and the same cache and
walk them differently.

The dispatcher test asked for a batched decode to be bit-exact, which batching never was: a
convolution blocks off the rows it is handed. Held to the same 1e-4 as the batching tests it
sits beside, which a misplaced tile would still miss by order one.
The stand-in asked for patch_size on its own, which leaves the decoder ending on three channels
where unpatchify wants twelve, and diffusers' own tiled_decode raised before the reimplemented
one was reached. Wan 2.2 carries the patch in its channels and in its spatial ratio as well as
in the flag, so the stand-in now does too.
pds-amd and others added 15 commits August 10, 2026 23:07
Keep height and width as the sole explicit tile-window controls so runner behavior has one unambiguous shape contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
…vae-tile-batching

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	tests/core/test_vae_parallel.py
#	tests/core/test_vae_tile_parallel.py
#	tests/core/test_vae_tiling.py
#	xfuser/core/utils/vae_parallel.py
#	xfuser/core/utils/vae_tile_parallel.py
#	xfuser/core/utils/vae_tiling.py
#	xfuser/model_executor/models/runner_models/base_model.py
Co-authored-by: Cursor <cursoragent@cursor.com>
Use exact rectangular windows and absolute per-axis overlap while
validating the DistVAE version and API contract at startup.

BREAKING CHANGE: replace --vae_tile_overlap with paired height and width
pixel flags and require DistVAE 0.0.0beta9.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	xfuser/model_executor/models/runner_models/ltx.py
#	xfuser/model_executor/models/runner_models/wan.py
Accept both coordinated and dedicated process groups during decoder parallelization, and keep rectangular tile guidance focused on the sharded axis.

Co-authored-by: Cursor <cursoragent@cursor.com>
Validate the canonical DistVAE API and route LTX stage tiling through
the shared VAE manager so runtime flags control every stage.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Run DistVAE setup once after post-load initialization so staged pipelines
share the same ordering. Remove runner-specific forwarding and brittle
source-structure tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Rely on the declared DistVAE version floor and direct public imports instead
of duplicating its API contract. Keep behavioral coverage while removing
compatibility archaeology and redundant integration tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep runner capability declarations concise where nearby comments only restate pipeline behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Exercise DistVAE's real latent-row semantics so narrow valid tiles cannot be rejected behind a mocked boundary.

Co-authored-by: Cursor <cursoragent@cursor.com>
Preserve dedicated raw groups and use the configured VAE subgroup for in-process decoding instead of falling back to the world group.

Co-authored-by: Cursor <cursoragent@cursor.com>
Turn decoder split failures into shape-aware guidance that suggests complete-tile parallelism when the VAE supports it.

Co-authored-by: Cursor <cursoragent@cursor.com>

@avjves avjves left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! It really clear things up as well as adds bunch of new features :)

Left a couple of comments still.

Comment thread xfuser/model_executor/models/runner_models/vae_manager.py Outdated
Comment thread xfuser/model_executor/models/runner_models/vae_manager.py Outdated
Comment thread xfuser/model_executor/models/runner_models/ltx.py
Comment thread xfuser/model_executor/models/runner_models/causal_wan.py
Comment thread xfuser/model_executor/models/runner_models/vae_manager.py Outdated
Comment thread xfuser/model_executor/models/runner_models/base_model.py Outdated

@avjves avjves left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me :)
It's now waiting for the DistVAE PR to merge. @pds-amd can you ping me when it's merged so I can merge this one? 😄

@pds-amd

pds-amd commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

@avjves - the associated DistVAE release has been published.

https://github.com/xdit-project/DistVAE/releases/tag/0.1.0

@avjves

avjves commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

LTX-2.5 support was merged. Can you still add the changes to that model card? :)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants