Signed-off-by: Dong Wang <dongw2019@gmail.com> Signed-off-by: sniper35 <dongw2019@gmail.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
478 lines
17 KiB
Python
478 lines
17 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
"""Distributed VAE patch/tile parallelism utilities."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
import torch
|
|
import torch.distributed as dist
|
|
from vllm.logger import init_logger
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
def _get_vae_spatial_scale_factor(vae: Any) -> int:
|
|
try:
|
|
block_out_channels = getattr(getattr(vae, "config", None), "block_out_channels", None)
|
|
if block_out_channels:
|
|
return 2 ** (len(block_out_channels) - 1)
|
|
except Exception:
|
|
pass
|
|
return 8
|
|
|
|
|
|
def _factor_pp_grid(pp_size: int) -> tuple[int, int]:
|
|
"""Pick a (rows, cols) grid whose product equals `pp_size`."""
|
|
if pp_size <= 1:
|
|
return (1, 1)
|
|
root = int(math.sqrt(pp_size))
|
|
for rows in range(root, 0, -1):
|
|
if pp_size % rows == 0:
|
|
return (rows, pp_size // rows)
|
|
return (1, pp_size)
|
|
|
|
|
|
def _get_world_rank_pp_size(
|
|
group: dist.ProcessGroup,
|
|
vae_patch_parallel_size: int,
|
|
) -> tuple[int, int, int]:
|
|
world_size = dist.get_world_size(group)
|
|
rank = dist.get_rank(group)
|
|
pp_size = min(int(vae_patch_parallel_size), int(world_size))
|
|
return world_size, rank, pp_size
|
|
|
|
|
|
def _get_vae_out_channels(vae: Any) -> int:
|
|
return int(getattr(getattr(vae, "config", None), "out_channels", 3))
|
|
|
|
|
|
def _get_vae_tile_params(vae: Any) -> tuple[int, float] | None:
|
|
tile_latent_min_size = getattr(vae, "tile_latent_min_size", None)
|
|
tile_overlap_factor = getattr(vae, "tile_overlap_factor", None)
|
|
if tile_latent_min_size is None or tile_overlap_factor is None:
|
|
return None
|
|
return int(tile_latent_min_size), float(tile_overlap_factor)
|
|
|
|
|
|
def _get_vae_tiling_params(vae: Any) -> tuple[int, float, int] | None:
|
|
tile_sample_min_size = getattr(vae, "tile_sample_min_size", None)
|
|
tile_params = _get_vae_tile_params(vae)
|
|
if tile_params is None or tile_sample_min_size is None:
|
|
return None
|
|
tile_latent_min_size, tile_overlap_factor = tile_params
|
|
return tile_latent_min_size, tile_overlap_factor, int(tile_sample_min_size)
|
|
|
|
|
|
def _distributed_tiled_decode(
|
|
*,
|
|
vae: Any,
|
|
orig_decode: Callable[..., Any],
|
|
z: torch.Tensor,
|
|
group: dist.ProcessGroup,
|
|
vae_patch_parallel_size: int,
|
|
) -> torch.Tensor:
|
|
"""Distributed version of diffusers AutoencoderKL.tiled_decode (decode only).
|
|
|
|
Each rank decodes a subset of tiles; rank0 gathers all tiles and performs the
|
|
original blend + stitch logic. Non-rank0 ranks return an empty tensor; callers
|
|
can broadcast the stitched result if needed.
|
|
"""
|
|
world_size, rank, pp_size = _get_world_rank_pp_size(group, vae_patch_parallel_size)
|
|
if pp_size <= 1:
|
|
return orig_decode(z, return_dict=False)[0]
|
|
|
|
tiling_params = _get_vae_tiling_params(vae)
|
|
if tiling_params is None:
|
|
return orig_decode(z, return_dict=False)[0]
|
|
tile_latent_min_size, tile_overlap_factor, tile_sample_min_size = tiling_params
|
|
|
|
overlap_size = int(tile_latent_min_size * (1 - tile_overlap_factor))
|
|
if overlap_size <= 0:
|
|
return orig_decode(z, return_dict=False)[0]
|
|
|
|
h_starts = list(range(0, z.shape[2], overlap_size))
|
|
w_starts = list(range(0, z.shape[3], overlap_size))
|
|
num_rows = len(h_starts)
|
|
num_cols = len(w_starts)
|
|
num_tiles = num_rows * num_cols
|
|
|
|
if num_tiles < 2:
|
|
return orig_decode(z, return_dict=False)[0]
|
|
|
|
blend_extent = int(tile_sample_min_size * tile_overlap_factor)
|
|
row_limit = int(tile_sample_min_size - blend_extent)
|
|
|
|
# Decide which ranks actively decode tiles.
|
|
active = rank < pp_size
|
|
|
|
local_tiles: list[torch.Tensor] = []
|
|
local_meta: list[tuple[int, int, int]] = []
|
|
|
|
tile_id = 0
|
|
for i in h_starts:
|
|
for j in w_starts:
|
|
# Offset assignment by 1 so rank0 avoids decoding the largest (tile_id=0) tile.
|
|
tile_rank = (tile_id + 1) % pp_size
|
|
if active and (tile_rank == rank):
|
|
tile = z[:, :, i : i + tile_latent_min_size, j : j + tile_latent_min_size]
|
|
if getattr(getattr(vae, "config", None), "use_post_quant_conv", False):
|
|
tile = vae.post_quant_conv(tile)
|
|
decoded = vae.decoder(tile)
|
|
local_tiles.append(decoded)
|
|
local_meta.append((tile_id, int(decoded.shape[-2]), int(decoded.shape[-1])))
|
|
tile_id += 1
|
|
|
|
# Gather per-rank tile counts.
|
|
count_tensor = torch.tensor([len(local_tiles)], device=z.device, dtype=torch.int64)
|
|
if rank == 0:
|
|
count_gather = [torch.empty_like(count_tensor) for _ in range(world_size)]
|
|
else:
|
|
count_gather = None
|
|
dist.gather(count_tensor, gather_list=count_gather, dst=0, group=group)
|
|
max_count = 0
|
|
if rank == 0:
|
|
counts = [int(t.item()) for t in count_gather] # type: ignore[arg-type]
|
|
max_count = max(counts) if counts else 0
|
|
max_count_tensor = torch.tensor([max_count], device=z.device, dtype=torch.int64)
|
|
dist.broadcast(max_count_tensor, src=0, group=group)
|
|
max_count = int(max_count_tensor.item())
|
|
|
|
out_channels = _get_vae_out_channels(vae)
|
|
|
|
# Prepare padded metadata + tiles for gather.
|
|
meta_tensor = torch.full((max_count, 3), -1, device=z.device, dtype=torch.int64)
|
|
tile_tensor = torch.zeros(
|
|
(max_count, z.shape[0], out_channels, tile_sample_min_size, tile_sample_min_size),
|
|
device=z.device,
|
|
dtype=z.dtype,
|
|
)
|
|
for idx, (tile_id, h, w) in enumerate(local_meta):
|
|
meta_tensor[idx, 0] = tile_id
|
|
meta_tensor[idx, 1] = h
|
|
meta_tensor[idx, 2] = w
|
|
tile_tensor[idx, :, :, :h, :w] = local_tiles[idx]
|
|
|
|
if rank == 0:
|
|
meta_gather = [torch.empty_like(meta_tensor) for _ in range(world_size)]
|
|
tile_gather = [torch.empty_like(tile_tensor) for _ in range(world_size)]
|
|
else:
|
|
meta_gather = None
|
|
tile_gather = None
|
|
|
|
dist.gather(meta_tensor, gather_list=meta_gather, dst=0, group=group)
|
|
dist.gather(tile_tensor, gather_list=tile_gather, dst=0, group=group)
|
|
|
|
if rank != 0:
|
|
return torch.empty(0, device=z.device, dtype=z.dtype)
|
|
|
|
# Reconstruct the full tile grid on rank0.
|
|
tile_map: dict[int, torch.Tensor] = {}
|
|
for src_rank in range(world_size):
|
|
meta_src = meta_gather[src_rank] # type: ignore[index]
|
|
tiles_src = tile_gather[src_rank] # type: ignore[index]
|
|
for idx in range(max_count):
|
|
tid = int(meta_src[idx, 0].item())
|
|
if tid < 0:
|
|
continue
|
|
h = int(meta_src[idx, 1].item())
|
|
w = int(meta_src[idx, 2].item())
|
|
tile_map[tid] = tiles_src[idx, :, :, :h, :w]
|
|
|
|
rows: list[list[torch.Tensor]] = []
|
|
for r in range(num_rows):
|
|
row: list[torch.Tensor] = []
|
|
for c in range(num_cols):
|
|
tid = r * num_cols + c
|
|
row.append(tile_map[tid])
|
|
rows.append(row)
|
|
|
|
result_rows: list[torch.Tensor] = []
|
|
for i, row in enumerate(rows):
|
|
result_row: list[torch.Tensor] = []
|
|
for j, tile in enumerate(row):
|
|
if i > 0:
|
|
tile = vae.blend_v(rows[i - 1][j], tile, blend_extent)
|
|
if j > 0:
|
|
tile = vae.blend_h(row[j - 1], tile, blend_extent)
|
|
result_row.append(tile[:, :, :row_limit, :row_limit])
|
|
result_rows.append(torch.cat(result_row, dim=3))
|
|
|
|
return torch.cat(result_rows, dim=2)
|
|
|
|
|
|
def _distributed_patch_decode(
|
|
*,
|
|
vae: Any,
|
|
orig_decode: Callable[..., Any],
|
|
z: torch.Tensor,
|
|
group: dist.ProcessGroup,
|
|
vae_patch_parallel_size: int,
|
|
vae_scale_factor: int,
|
|
) -> torch.Tensor:
|
|
"""Decode one spatial block per rank, then stitch RGB blocks on rank0.
|
|
|
|
Intended for sizes where diffusers tiling would not kick in, so we can still
|
|
reduce the per-rank VAE decode activation peak. Each rank decodes a core
|
|
block with a small latent-space halo, then crops to the core and gathers the
|
|
RGB blocks to rank0 for final stitching.
|
|
"""
|
|
world_size, rank, pp_size = _get_world_rank_pp_size(group, vae_patch_parallel_size)
|
|
if pp_size <= 1:
|
|
return orig_decode(z, return_dict=False)[0]
|
|
|
|
tile_params = _get_vae_tile_params(vae)
|
|
if tile_params is None:
|
|
return orig_decode(z, return_dict=False)[0]
|
|
tile_latent_min_size, tile_overlap_factor = tile_params
|
|
|
|
overlap_latent = int(tile_latent_min_size * float(tile_overlap_factor))
|
|
halo_base = max(0, overlap_latent // 2)
|
|
|
|
# Only ranks < pp_size participate in decoding. Others send empty tensors.
|
|
active = rank < pp_size
|
|
|
|
bsz, _, latent_h, latent_w = z.shape
|
|
scale = int(vae_scale_factor)
|
|
out_h = latent_h * scale
|
|
out_w = latent_w * scale
|
|
|
|
out_channels = _get_vae_out_channels(vae)
|
|
|
|
local_core = torch.empty(0, device=z.device, dtype=z.dtype)
|
|
local_h = 0
|
|
local_w = 0
|
|
|
|
grid_rows, grid_cols = _factor_pp_grid(pp_size)
|
|
|
|
if active:
|
|
patch_idx = rank
|
|
patch_row = patch_idx // grid_cols
|
|
patch_col = patch_idx % grid_cols
|
|
|
|
h0 = (patch_row * latent_h) // grid_rows
|
|
h1 = ((patch_row + 1) * latent_h) // grid_rows
|
|
w0 = (patch_col * latent_w) // grid_cols
|
|
w1 = ((patch_col + 1) * latent_w) // grid_cols
|
|
|
|
core_h = max(0, h1 - h0)
|
|
core_w = max(0, w1 - w0)
|
|
if core_h == 0 or core_w == 0:
|
|
local_core = torch.empty(0, device=z.device, dtype=z.dtype)
|
|
else:
|
|
halo = max(halo_base, min(core_h, core_w) // 2)
|
|
ph0 = max(0, h0 - halo)
|
|
ph1 = min(latent_h, h1 + halo)
|
|
pw0 = max(0, w0 - halo)
|
|
pw1 = min(latent_w, w1 + halo)
|
|
|
|
tile = z[:, :, ph0:ph1, pw0:pw1]
|
|
if getattr(getattr(vae, "config", None), "use_post_quant_conv", False):
|
|
tile = vae.post_quant_conv(tile)
|
|
decoded = vae.decoder(tile)
|
|
|
|
ch0 = (h0 - ph0) * scale
|
|
cw0 = (w0 - pw0) * scale
|
|
ch1 = ch0 + core_h * scale
|
|
cw1 = cw0 + core_w * scale
|
|
local_core = decoded[:, :, ch0:ch1, cw0:cw1]
|
|
|
|
local_h = int(local_core.shape[-2]) if local_core.numel() else 0
|
|
local_w = int(local_core.shape[-1]) if local_core.numel() else 0
|
|
|
|
# Gather block shapes.
|
|
shape_tensor = torch.tensor([local_h, local_w], device=z.device, dtype=torch.int64)
|
|
if rank == 0:
|
|
shape_gather = [torch.empty_like(shape_tensor) for _ in range(world_size)]
|
|
else:
|
|
shape_gather = None
|
|
dist.gather(shape_tensor, gather_list=shape_gather, dst=0, group=group)
|
|
|
|
max_h = 0
|
|
max_w = 0
|
|
if rank == 0:
|
|
shapes = [tuple(int(x.item()) for x in t) for t in shape_gather] # type: ignore[arg-type]
|
|
max_h = max((h for h, _ in shapes), default=0)
|
|
max_w = max((w for _, w in shapes), default=0)
|
|
|
|
max_hw_tensor = torch.tensor([max_h, max_w], device=z.device, dtype=torch.int64)
|
|
dist.broadcast(max_hw_tensor, src=0, group=group)
|
|
max_h = int(max_hw_tensor[0].item())
|
|
max_w = int(max_hw_tensor[1].item())
|
|
|
|
# Pad local block for gather.
|
|
if max_h == 0 or max_w == 0:
|
|
padded = torch.empty(0, device=z.device, dtype=z.dtype)
|
|
else:
|
|
padded = torch.zeros((bsz, out_channels, max_h, max_w), device=z.device, dtype=z.dtype)
|
|
if local_h and local_w:
|
|
padded[:, :, :local_h, :local_w] = local_core
|
|
|
|
if rank == 0:
|
|
block_gather = [torch.empty_like(padded) for _ in range(world_size)]
|
|
else:
|
|
block_gather = None
|
|
dist.gather(padded, gather_list=block_gather, dst=0, group=group)
|
|
|
|
if rank != 0:
|
|
return torch.empty(0, device=z.device, dtype=z.dtype)
|
|
|
|
# Stitch on rank0.
|
|
out = torch.empty((bsz, out_channels, out_h, out_w), device=z.device, dtype=z.dtype)
|
|
|
|
for patch_idx in range(pp_size):
|
|
src_rank = patch_idx
|
|
patch_row = patch_idx // grid_cols
|
|
patch_col = patch_idx % grid_cols
|
|
|
|
h0 = (patch_row * latent_h) // grid_rows
|
|
h1 = ((patch_row + 1) * latent_h) // grid_rows
|
|
w0 = (patch_col * latent_w) // grid_cols
|
|
w1 = ((patch_col + 1) * latent_w) // grid_cols
|
|
|
|
ph = (h1 - h0) * scale
|
|
pw = (w1 - w0) * scale
|
|
if ph <= 0 or pw <= 0:
|
|
continue
|
|
|
|
tile = block_gather[src_rank] # type: ignore[index]
|
|
out[:, :, h0 * scale : h1 * scale, w0 * scale : w1 * scale] = tile[:, :, :ph, :pw]
|
|
|
|
return out
|
|
|
|
|
|
class VaePatchParallelism:
|
|
"""Patch/tile-parallel VAE decode wrapper.
|
|
|
|
This is meant to wrap `vae.decode` as an instance-level override
|
|
so pipelines don't need model-specific code paths.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
vae: Any,
|
|
*,
|
|
vae_patch_parallel_size: int,
|
|
group_getter: Callable[[], dist.ProcessGroup],
|
|
) -> None:
|
|
self._vae = vae
|
|
self._vae_patch_parallel_size = int(vae_patch_parallel_size)
|
|
self._group_getter = group_getter
|
|
|
|
self._vae_scale_factor = _get_vae_spatial_scale_factor(vae)
|
|
self._orig_decode = vae.decode
|
|
|
|
def decode(self, z: torch.Tensor, return_dict: bool = True, *args: Any, **kwargs: Any):
|
|
# Keep the original path for unsupported VAE types / shapes.
|
|
if z.ndim != 4:
|
|
return self._orig_decode(z, return_dict=return_dict, *args, **kwargs)
|
|
|
|
if self._vae_patch_parallel_size <= 1 or not dist.is_initialized():
|
|
return self._orig_decode(z, return_dict=return_dict, *args, **kwargs)
|
|
|
|
if not getattr(self._vae, "use_tiling", False):
|
|
return self._orig_decode(z, return_dict=return_dict, *args, **kwargs)
|
|
|
|
try:
|
|
group = self._group_getter()
|
|
except Exception:
|
|
return self._orig_decode(z, return_dict=return_dict, *args, **kwargs)
|
|
|
|
world_size = dist.get_world_size(group)
|
|
rank = dist.get_rank(group)
|
|
pp_size = min(int(self._vae_patch_parallel_size), int(world_size))
|
|
if pp_size <= 1:
|
|
return self._orig_decode(z, return_dict=return_dict, *args, **kwargs)
|
|
|
|
# Match diffusers' condition for when VAE tiling would be used.
|
|
tile_latent_min_size = getattr(self._vae, "tile_latent_min_size", None)
|
|
if tile_latent_min_size is None:
|
|
decoded = _distributed_tiled_decode(
|
|
vae=self._vae,
|
|
orig_decode=self._orig_decode,
|
|
z=z,
|
|
group=group,
|
|
vae_patch_parallel_size=pp_size,
|
|
)
|
|
else:
|
|
should_tile = (z.shape[-1] > tile_latent_min_size) or (z.shape[-2] > tile_latent_min_size)
|
|
if should_tile:
|
|
decoded = _distributed_tiled_decode(
|
|
vae=self._vae,
|
|
orig_decode=self._orig_decode,
|
|
z=z,
|
|
group=group,
|
|
vae_patch_parallel_size=pp_size,
|
|
)
|
|
else:
|
|
decoded = _distributed_patch_decode(
|
|
vae=self._vae,
|
|
orig_decode=self._orig_decode,
|
|
z=z,
|
|
group=group,
|
|
vae_patch_parallel_size=pp_size,
|
|
vae_scale_factor=self._vae_scale_factor,
|
|
)
|
|
|
|
if rank == 0 and decoded.numel() == 0:
|
|
logger.warning("VAE patch parallel decode produced empty output on rank0; falling back to vae.decode.")
|
|
decoded = self._orig_decode(z, return_dict=False, *args, **kwargs)[0]
|
|
if rank == 0 and decoded.dtype != z.dtype:
|
|
decoded = decoded.to(dtype=z.dtype)
|
|
if rank == 0 and not decoded.is_contiguous():
|
|
decoded = decoded.contiguous()
|
|
|
|
shape_tensor = torch.empty((4,), device=z.device, dtype=torch.int64)
|
|
if rank == 0:
|
|
shape_tensor.copy_(torch.tensor(tuple(decoded.shape), device=z.device, dtype=torch.int64))
|
|
dist.broadcast(shape_tensor, src=0, group=group)
|
|
|
|
if rank != 0:
|
|
decoded = torch.empty(tuple(int(x) for x in shape_tensor.tolist()), device=z.device, dtype=z.dtype)
|
|
dist.broadcast(decoded, src=0, group=group)
|
|
|
|
if not return_dict:
|
|
return (decoded,)
|
|
|
|
from diffusers.models.autoencoders.vae import DecoderOutput
|
|
|
|
return DecoderOutput(sample=decoded)
|
|
|
|
|
|
def maybe_wrap_vae_decode_with_patch_parallelism(
|
|
pipeline: Any,
|
|
*,
|
|
vae_patch_parallel_size: int,
|
|
group_getter: Callable[[], dist.ProcessGroup],
|
|
) -> None:
|
|
"""Wrap a diffusers-style pipeline's `vae.decode` with patch/tile parallel decode."""
|
|
if vae_patch_parallel_size <= 1:
|
|
return
|
|
|
|
vae = getattr(pipeline, "vae", None)
|
|
if vae is None or not hasattr(vae, "decode"):
|
|
return
|
|
# NOTE: Use capability checks (not strict diffusers type checks) because
|
|
# NextStep's custom VAE is compatible but not a diffusers AutoencoderKL.
|
|
# TODO(vae-pp): Replace duck-typing with an explicit VAE compatibility
|
|
# validator/protocol and add per-model integration tests.
|
|
if not hasattr(vae, "decoder"):
|
|
return
|
|
|
|
if getattr(vae, "_vllm_vae_patch_parallel_installed", False):
|
|
return
|
|
|
|
wrapper = VaePatchParallelism(
|
|
vae,
|
|
vae_patch_parallel_size=vae_patch_parallel_size,
|
|
group_getter=group_getter,
|
|
)
|
|
|
|
vae._vllm_vae_patch_parallel_installed = True # type: ignore[attr-defined]
|
|
vae._vllm_vae_patch_parallel_original_decode = vae.decode # type: ignore[attr-defined]
|
|
vae.decode = wrapper.decode # type: ignore[assignment]
|