Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
8675416
test: make the distributed tests runnable without an accelerator
pds-amd Aug 4, 2026
c8e49ed
fix(groupnorm): reduce the variance about the group mean, not the rank's
pds-amd Aug 4, 2026
039d655
test: cover the two existing decoder adapters without a GPU
pds-amd Aug 4, 2026
67871e3
feat(qwenimage): shard the Qwen-Image VAE decoder
pds-amd Aug 4, 2026
a66bd6d
fix(patchconv): stop cropping a halo that was already spent on padding
pds-amd Aug 4, 2026
eed8603
feat(hunyuanvideo): shard the HunyuanVideo and HunyuanVideo 1.5 VAE d…
pds-amd Aug 4, 2026
103ec61
feat(ltx2): shard the LTX-2 VAE decoder
pds-amd Aug 4, 2026
49310aa
docs(setup): say why the diffusers floor stays at 0.35
pds-amd Aug 4, 2026
4bda9b3
fix(patchify): split rows unevenly instead of padding them to divide
pds-amd Aug 4, 2026
9ed7caa
feat(qwenimage): shard the Qwen-Image VAE encoder
pds-amd Aug 4, 2026
9b87a9d
feat(hunyuanvideo): shard the HunyuanVideo and HunyuanVideo 1.5 VAE e…
pds-amd Aug 4, 2026
b0dc649
feat(ltx2): shard the LTX-2 VAE encoder
pds-amd Aug 4, 2026
46fadd0
test: keep the gloo rendezvous port below the ephemeral range
pds-amd Aug 4, 2026
69b0eae
feat(vae): shard the 2D VAE encoder
pds-amd Aug 4, 2026
59b994f
bench: measure the sharded halves at real shapes without a checkpoint
pds-amd Aug 5, 2026
4ec66ff
bench: reach the state a real run is already in before measuring it
pds-amd Aug 5, 2026
ea4926e
perf(groupnorm): send the row count with the group sums
pds-amd Aug 5, 2026
e66915b
perf(conv): exchange both halos in one batch
pds-amd Aug 5, 2026
e6d1ef9
bench: keep a batch from being charged for the round trips it avoids
pds-amd Aug 5, 2026
3e90ba0
bench: judge agreement against the reference scale, at a tolerance th…
pds-amd Aug 5, 2026
a018897
bench: decode a batch of latents, standing in for batched tiles
pds-amd Aug 5, 2026
e34d0ee
refactor: drop use_uniform_patch, and the dead norm that came with it
pds-amd Aug 5, 2026
92eb1d3
bench: cover every VAE family, both halves, and the busiest rank
pds-amd Aug 5, 2026
d174390
perf(conv): let a unit-stride conv work out its halo without asking
pds-amd Aug 5, 2026
e1d0c4e
test: compare the halo shortcut only where the gathered one is defined
pds-amd Aug 5, 2026
41b60ea
test(wan): make the gloo conv test leave together, and land on the sa…
pds-amd Aug 5, 2026
27176a8
feat(bench): measure the tiling arms, by xDiT's own calls in its own …
pds-amd Aug 5, 2026
a4562b9
feat(bench): put a number on how far an arm lands from an untiled uns…
pds-amd Aug 5, 2026
13ca1d1
feat(bench): measure a whole grid of arms and shapes in one process
pds-amd Aug 5, 2026
1d5a76f
perf(dist): stop issuing collectives a one-rank VAE group cannot bene…
pds-amd Aug 5, 2026
2a4e1d4
test(norm): ask PatchGroupNorm to round no worse than the norm it rep…
pds-amd Aug 6, 2026
3dc2562
bench: time a tiled decode by phase, so the blend can be read apart f…
pds-amd Aug 6, 2026
38107cc
bench: add --tile-split scattered, so bands can be measured against t…
pds-amd Aug 6, 2026
d9fe682
bench: time the decoder itself, and report the spread across ranks
pds-amd Aug 6, 2026
096f156
Add a main-vs-main arm, and drop the batching flags with it
pds-amd Aug 6, 2026
4dc6550
Give the plain AutoencoderKL the window a checkpoint ships
pds-amd Aug 6, 2026
f893e85
Report a failed cell from whichever rank failed it
pds-amd Aug 6, 2026
27755a9
Measure the tile overlap, which is the lever the window is not
pds-amd Aug 7, 2026
0781587
Give each decode its own place in the feature cache
pds-amd Aug 7, 2026
ab908b1
Ask only our own adapters not to default a mutable argument
pds-amd Aug 7, 2026
040addd
Refuse a band too thin to lend a halo where every rank refuses together
pds-amd Aug 7, 2026
1838f90
Say what produced a measurement, so a report can travel
pds-amd Aug 7, 2026
ccae00c
Let an overlap arm ask for half of whatever the VAE's own is
pds-amd Aug 7, 2026
5b88994
Let a group norm find out which axis the run splits on
pds-amd Aug 7, 2026
16cf591
Say which axis a norm splits on, and check the one an encoder was told
pds-amd Aug 7, 2026
610cced
Make the test files that collect nothing say so, and the halo tests n…
pds-amd Aug 7, 2026
5611d49
Stop the causal encoder counting a narrowing it cannot see
pds-amd Aug 7, 2026
4220756
Check the stride-2 off-by-one rather than expecting it
pds-amd Aug 7, 2026
452bcf9
Delete a stride alignment that never aligned anything, and cut the ch…
pds-amd Aug 7, 2026
9a8daa4
Fix the third caller of the halo metadata, and chunk above the kernel
pds-amd Aug 7, 2026
a20917d
Wrap the 2D residual block where it stands, and stop each cell carryi…
pds-amd Aug 7, 2026
89a555a
Never cut a chunk the convolution cannot be run on
pds-amd Aug 7, 2026
9b2588c
Fix omitted cache cursors and describe-only runs
pds-amd Aug 10, 2026
5210428
Keep VAE parallel state adapter-local
pds-amd Aug 10, 2026
4f7e872
Own VAE sharding and tiling policy
pds-amd Aug 10, 2026
20e154e
Make VAE benchmark runner-independent
pds-amd Aug 10, 2026
9486985
Separate VAE adaptation from instrumentation
pds-amd Aug 10, 2026
c545f3e
Version the public VAE orchestration API
pds-amd Aug 10, 2026
9235fd9
Tighten VAE tests and invariants
pds-amd Aug 10, 2026
1da6688
Enforce structural benchmark agreement
pds-amd Aug 10, 2026
1509aa0
Cover uneven Wan zero-pad bands
pds-amd Aug 10, 2026
e2c3107
Support rectangular VAE tile windows
pds-amd Aug 10, 2026
c29549f
Support strip-shaped VAE tiles
pds-amd Aug 11, 2026
616577e
Merge remote-tracking branch 'origin/feat/vae-bench-harness' into int…
pds-amd Aug 11, 2026
eeac96a
Merge branch 'feat/vae-bench-harness' into integrate/vae-bench-push
pds-amd Aug 11, 2026
c6567b2
Escape tile scheduler local optima
pds-amd Aug 11, 2026
bd3df86
Preserve adapted decoder runtime state
pds-amd Aug 11, 2026
7adaa5e
Simplify distributed VAE adapters
pds-amd Aug 11, 2026
bb286e8
Simplify VAE tiling controls
pds-amd Aug 11, 2026
6297c80
Simplify benchmark execution modes
pds-amd Aug 11, 2026
e2200b3
Redesign benchmark cases around Pareto tiles
pds-amd Aug 11, 2026
62f5d61
Generalize asymmetric zero-pad convolution
pds-amd Aug 11, 2026
68d57a5
Search tile overlap instead of pinning it to the VAE native
pds-amd Aug 11, 2026
9ebe500
Bound the narrow tile axis away from banding
pds-amd Aug 11, 2026
a7665f7
Keep every blend above a quarter of its window
pds-amd Aug 11, 2026
6a21457
Update the overlap ladder test to the quarter-of-window bound
pds-amd Aug 11, 2026
25f40de
Stop the memory profile returning throughput's transpose
pds-amd Aug 11, 2026
aa55691
Default the suite to the compositions a caller can select
pds-amd Aug 11, 2026
f4ac69f
Select the throughput plan on window area, not total work
pds-amd Aug 11, 2026
d10d59d
Name the tile profiles for geometry, not for predicted outcome
pds-amd Aug 11, 2026
fe4fd74
Carry the shape matrix and the measured device in the harness
pds-amd Aug 11, 2026
cd8186c
Harden reproducible benchmark sweeps
pds-amd Aug 12, 2026
12043df
Warn when tile ranks repeat work
pds-amd Aug 12, 2026
89822f4
Harden dependency compatibility
pds-amd Aug 12, 2026
cdc8e80
Refine VAE benchmark plan selection
pds-amd Aug 12, 2026
0a640dc
Merge remote benchmark updates
pds-amd Aug 12, 2026
f96e41d
Clarify distributed VAE decode strategies
pds-amd Aug 12, 2026
da0c17e
Remove legacy adapter model copies
pds-amd Aug 12, 2026
90fac5f
Remove implementation-detail tests
pds-amd Aug 12, 2026
9165ee0
Fix rectangular latent row lookup
pds-amd Aug 13, 2026
fa6cd5c
Preserve rectangular latent geometry
pds-amd Aug 13, 2026
500ef13
Expose xDiT VAE orchestration helpers
pds-amd Aug 13, 2026
764e23d
Validate rectangular tile call counts
pds-amd Aug 13, 2026
17fcce7
Type VAE row split failures
pds-amd Aug 13, 2026
c3cfcfb
fix(workflows): update renamed adapter compatibility test
pds-amd Aug 17, 2026
3da9f4f
fix(setup): scope find_packages to distvae namespace
pds-amd Aug 17, 2026
316e9fd
fix: cache tile share assignments
pds-amd Aug 17, 2026
1804be2
fix: share safe convolution chunking
pds-amd Aug 18, 2026
1ce25b5
docs: describe distributed group normalization
pds-amd Aug 18, 2026
01161ab
docs: explain patch shard materialization
pds-amd Aug 18, 2026
15cd143
fix: bound tile assignment refinement
pds-amd Aug 18, 2026
5000830
fix: bound distributed test hangs
pds-amd Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Test

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
minimum-dependencies:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
OMP_NUM_THREADS: "1"
MKL_NUM_THREADS: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install minimum supported dependencies
run: |
python -m pip install \
"numpy<2" \
"torch==2.2.*" \
"diffusers==0.30.3" \
"huggingface-hub<0.26" \
pytest \
packaging
python -m pip install --no-deps -e .
- name: Test minimum dependency boundary
run: |
python -m pytest -vv --durations=20 \
test/test_adapter_compatibility.py \
test/test_public_vae_api.py \
test/test_decoderadapter.py \
test/test_encoderadapter.py

latest-dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install latest dependencies
run: |
python -m pip install -e .
python -m pip install pytest
- name: Run test suite
run: python -m pytest -q
255 changes: 211 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,227 @@
# DistVAE: A patch parallelism distributed VAE implement for high resolution generation
# DistVAE

By providing a set of adapter interfaces, this project allows users to quickly convert vae-related implementations in the diffusers library into parallel versions on multiple gpu's, enabling non-intrusive parallelisation of the vae portion of an existing model, thus reducing the memory footprint of the image generation process, and avoiding vae-induced memory spikes.
DistVAE replaces supported diffusers VAE encoders and decoders with distributed adapters. The rest
of the diffusion pipeline stays unchanged.

## Installation

``` bash
```bash
pip install distvae
```

## Usage
Python 3.10 or newer, with `torch>=2.2` and `diffusers>=0.30.3`. Individual VAE families may require
a newer Diffusers release.

Refering to the file in `test/` directory. In general, you only need to use the corresponding adapter for the diffusers module to make it work on multiple gpu in parallel.
The pipeline quickstart also needs Transformers:

As an example, we can transform an initialised vae decoder into a parallel versions:
```bash
pip install "distvae[pipeline]"
```

## Quickstart

``` python
from diffusers.models.autoencoders.vae import Decoder
from distvae.modules.adapters.vae.decoder_adapters import DecoderAdapter
Every rank builds the same pipeline, and DistVAE shards the VAE inside it. Save this as `decode.py`:

```python
import os

import torch
import random
import torch.distributed as dist
from diffusers import DiffusionPipeline
from distvae import vae as vae_api

dist.init_process_group(backend="nccl")
device = torch.device(f"cuda:{os.environ['LOCAL_RANK']}")
torch.cuda.set_device(device)

# The group the VAE is split over. Every rank that enters the VAE call must be a
# member. If you create a subgroup, gate the pipeline call to those ranks too.
vae_group = dist.group.WORLD

pipe = DiffusionPipeline.from_pretrained(
os.environ["MODEL_ID"], torch_dtype=torch.bfloat16
).to(device)

def set_seed(seed: int = 42):
random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)

@torch.no_grad()
def main():
# init
set_seed()
dist.init_process_group(backend="nccl")
rank = dist.get_rank()
torch.device('cuda', rank)

# input
hidden_state = torch.randn(1, 4, 128, 128, device=f"cuda:{rank}")
# create vae.decoder instance
decoder = Decoder(
in_channels=4, out_channels=3,
up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D", "UpDecoderBlock2D", "UpDecoderBlock2D"],
block_out_channels=(128, 256, 512, 512), layers_per_block=2,
norm_num_groups=32, act_fn="silu",
).to(f"cuda:{rank}")
# transform vae.decoder to distvae.decoder
patch_decoder = DecoderAdapter(decoder).to(f"cuda:{rank}")
# forward
result = decoder(hidden_state)
patch_result = patch_decoder(hidden_state)

print("result shape: ", patch_result.shape)
if rank == 0:
assert torch.allclose(result, patch_result, atol=1e-2), "two hidden states are not equal"

if __name__ == "__main__":
main()
vae_api.parallelize_decoder(pipe.vae, vae_group)
vae_api.parallelize_encoder(pipe.vae, vae_group)

image = pipe("A cat holding a sign that says hello world", height=1024, width=1024).images[0]
if dist.get_rank() == 0:
image.save("out.png")
```

Then launch it across your GPUs with any pipeline whose VAE DistVAE supports. For example, with a
recent Diffusers release:

```bash
MODEL_ID=black-forest-labs/FLUX.2-dev torchrun --nproc_per_node=4 decode.py
```

Both calls raise if there is no adapter for the VAE, so an unsupported model fails at setup rather
than part way through a decode.

## Supported VAEs

Every family below supports both row sharding and tiling. Qwen-Image is listed with the video VAEs
because its Wan-derived autoencoder has a frame axis.

| VAE | Frame axis | Tiles by | A tile is |
| ---------------- | ---------- | ----------------------- | -------------------------------------------------- |
| `AutoencoderKL` | no | overlap-derived strides | one decoder call |
| Flux.2 | no | overlap-derived strides | one decoder call |
| HunyuanVideo 1.5 | yes | overlap-derived strides | one decoder call |
| HunyuanVideo | yes | a stored stride | one decoder call per temporal chunk |
| LTX-2 | yes | a stored stride | one decoder call unless temporal tiling is enabled |
| Wan | yes | a stored stride | a call per frame, threading a causal cache |
| Qwen-Image | yes | a stored stride | a call per frame, threading a causal cache |

Tile size affects the families differently. A smaller tile reduces tile-local activation memory when
one tile is one decoder call, but allocations outside the spatial tile can determine the measured
peak. Wan and Qwen-Image decode one frame at a time, so their peak memory is often set by temporal
state.

`tile_overlap_plan` accepts exact output-pixel `(height, width)` values and maps them to each VAE's
stride settings. DistVAE owns the tiling loop for every family in the table. CogVideoX is excluded
because it tiles frames inside the spatial loop, so its spatial tiles are not independent.

## Distributed decode strategies

DistVAE provides two distributed decode strategies:

- **Row sharding** gives each rank a band in every adapted layer. It exchanges convolution halos and
normalization statistics, preserves the unsharded result within numerical tolerance, and usually
reduces activation memory as ranks are added.
- **Whole-tile distribution** gives each rank complete windows. Ranks exchange tile-edge data and
gather decoded pieces for assembly. Peak activation memory usually follows the tile window,
including on one GPU, while overlap repeats work and tile-local normalization can change the
output.

The figure compares the two distributed paths at two tile sizes. Each row reports peak activations,
decoded work, seams, load imbalance, and synchronization:

![Row sharding and two whole-tile distributions for a 1024 by 1024 image on four GPUs, compared by peak activations, work, seams, load imbalance, and synchronization](docs/figure.png)

[Choosing a decode path](docs/strategies.md) explains how VAE family, input shape, rank count, and
interconnect affect the choice. The [benchmark guide](bench/README.md) shows how to measure both
strategies against a vanilla unsharded Diffusers decode.

## Usage

The quickstart uses `distvae.vae`, which picks the adapter for a whole VAE. To shard a single
diffusers module instead, wrap it in its adapter:

```python
import os

import torch
import torch.distributed as dist
from diffusers.models.autoencoders.vae import Decoder
from distvae.modules.adapters.vae.decoder_adapters import DecoderAdapter

dist.init_process_group(backend="nccl")
local_rank = int(os.environ["LOCAL_RANK"])
device = torch.device(f"cuda:{local_rank}")
torch.cuda.set_device(device)
torch.manual_seed(42) # every rank must build the same weights and the same input

decoder = Decoder(
in_channels=4, out_channels=3,
up_block_types=["UpDecoderBlock2D"] * 4,
block_out_channels=(128, 256, 512, 512), layers_per_block=2,
norm_num_groups=32, act_fn="silu",
).to(device)

hidden_state = torch.randn(1, 4, 128, 128, device=device)
with torch.no_grad():
expected = decoder(hidden_state)

# The adapter takes ownership of decoder and replaces its distributed layers in
# place. Do not use decoder as an unmodified reference after this call.
patch_decoder = DecoderAdapter(decoder, dist.group.WORLD).to(device)
with torch.no_grad():
assert torch.allclose(expected, patch_decoder(hidden_state), atol=1e-2)
```

There are more runnable examples in `test/`.

### Tiling

Diffusers decides whether to tile. DistVAE resizes the window and distributes the tiles across the
group:

```python
from distvae import vae as vae_api

vae_api.require_vae_support(pipe.vae, "tiling", "enable_tiling()")
pipe.vae.enable_tiling()

# Optional: ask for an exact 192x192px window. Invalid shapes are refused rather
# than silently changed.
plan = vae_api.tile_shape_plan(pipe.vae, 192, 192)
if plan is None:
raise ValueError("this VAE cannot use a 192x192px tile shape")
vae_api.apply_tile_plan(pipe.vae, plan)

# Optional: overlap neighbouring tiles by 32 output pixels vertically and 64
# horizontally. This reads the window now set on the VAE, so apply it second.
step = vae_api.tile_overlap_plan(
pipe.vae, 32, 64, sample_shape=(1024, 1024)
)
if step is None:
raise ValueError("this VAE cannot use a 32x64px tile overlap")
vae_api.apply_tile_plan(pipe.vae, step)
replacement = vae_api.tiled_decode_for(pipe.vae)
if replacement is not None:
pipe.vae.tiled_decode = replacement

# Decode the tiles across the group instead of one after another.
if not vae_api.supports_tile_parallel(pipe.vae):
raise ValueError("this VAE does not support distributed tiled decode")
dispatch, assemble = vae_api.sharing(vae_group)
tiled_decode = vae_api.tiled_decode_for(pipe.vae, dispatch, assemble)
if tiled_decode is None:
raise ValueError("no distributed tiled decode is available for this VAE")
pipe.vae.tiled_decode = tiled_decode
```

Window and overlap are separate controls in output pixels. The window sets the memory required for
one tile. The overlap reduces the stride and increases repeated work.

Both planners return `None` when a request cannot be represented exactly. Apply `tile_shape_plan`
first because `tile_overlap_plan` reads the current tile shape. Requested overlap values are never
rounded.

[Choosing a tile window](docs/tiling.md) explains rectangular windows, clipped edge tiles, and
overlap.

### xDiT integration

xDiT chooses the tile settings and calls the DistVAE planners. Supply `vae_tile_overlap_height` and
`vae_tile_overlap_width` together in output pixels. Use zero on an axis that is not tiled.
Installing new shape or overlap settings replaces the previous tiled decode callable.

## Performance

Latency and memory depend on the VAE family, input shape, rank count, device, and interconnect. The
benchmark chooses up to three rectangular plans and records their work, memory estimate, and load
imbalance before running them. See `bench/README.md` for the suite and its limits.

## Development

```bash
git clone https://github.com/xdit-project/DistVAE
cd DistVAE
pip install -e ".[dev]"
mdformat --extensions gfm --wrap 100 README.md bench/README.md docs/*.md
pytest
```

Tests marked `gloo` spawn several ranks over gloo and need no accelerator, so `pytest -m gloo`
exercises the distributed paths on a CPU-only machine.

`docs/make_figure.py` regenerates `docs/figure.svg` and, when `cairosvg` is installed,
`docs/figure.png`.

## License

MIT. See `LICENSE.txt`.
Loading
Loading