# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project # SPDX-FileCopyrightText: Copyright 2026 Lightricks and The HuggingFace Team. All rights reserved. # # Distributed tiling is copied and modified from Diffusers' serial LTX-2.5 # tiling at commit d035dcd7cc7c88e0a154609b62887d50bba9fdc2 (Apache-2.0). """LTX-2.5-specific distributed execution for the diffusion VAE decoder.""" from __future__ import annotations import math from dataclasses import dataclass from typing import Any import torch import torch.distributed as dist from diffusers.utils import logging from diffusers.utils.torch_utils import randn_tensor from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor import ( DistributedOperator, DistributedVaeMixin, GridSpec, TileTask, ) from .ltx2_diffusion_decoder import LTX2VideoDiffusionDecoderModel logger = logging.get_logger(__name__) # pylint: disable=invalid-name def _tile_intervals(length: int, tile_size: int, stride: int, min_size: int) -> list[tuple[int, int]]: """Build overlapping intervals, merging a too-small trailing remnant.""" if length <= tile_size: return [(0, length)] starts = list(range(0, length, stride)) while len(starts) > 1 and length - starts[-1] < min_size: starts.pop() return [(start, min(start + tile_size, length)) for start in starts[:-1]] + [(starts[-1], length)] @dataclass(frozen=True) class LTX2VideoDiffusionTilePlan: """Geometry shared by all ranks for one distributed DiffVAE decode.""" temporal_tiles: tuple[tuple[int, int], ...] height_tiles: tuple[tuple[int, int], ...] width_tiles: tuple[tuple[int, int], ...] num_frames: int height: int width: int scale_t: int scale_h: int scale_w: int stride_t: int stride_h: int stride_w: int blend_frames: int blend_height: int blend_width: int single_step_x0: bool @dataclass class LTX2VideoDiffusionTileTask(TileTask): """One stage-4 + stage-5 tile and its rank-independent noise.""" drop_leading_frame: bool = False crop_trailing_ghost: bool = False noise_generator: torch.Generator | list[torch.Generator] | None = None x_t: torch.Tensor | None = None class DistributedLTX2VideoDiffusionDecoderModel(LTX2VideoDiffusionDecoderModel, DistributedVaeMixin): """LTX-2.5 diffusion decoder with model-specific tile parallelism. Stages 1-3 run over the complete low-resolution feature volume on every participating rank. Stage 4 and the diffusion stage run as independent overlapping tile tasks, and rank 0 performs the reference blend/merge. """ @classmethod def from_pretrained(cls, *args: Any, **kwargs: Any): model = super().from_pretrained(*args, **kwargs) model.init_distributed() return model def set_parallel_size(self, parallel_size: int, mode: str = "tile") -> None: if mode != "tile": raise ValueError(f"LTX-2.5 DiffVAE only supports vae_parallel_mode='tile', got {mode!r}.") super().set_parallel_size(parallel_size, mode=mode) def _build_tiled_decode_plan( self, features: torch.Tensor, num_inference_steps: int, ) -> LTX2VideoDiffusionTilePlan: """Build the rank-independent tile schedule around Diffusers' decoder.""" decoder = self.decoder patch_size = decoder.patch_size upsample_stride = decoder.upsamples[-1].stride scale_t, scale_h, scale_w = ( upsample_stride[0], upsample_stride[1] * patch_size, upsample_stride[2] * patch_size, ) tile_t = self.tile_sample_min_num_frames // scale_t tile_h = self.tile_sample_min_height // scale_h tile_w = self.tile_sample_min_width // scale_w stride_t = self.tile_sample_stride_num_frames // scale_t stride_h = self.tile_sample_stride_height // scale_h stride_w = self.tile_sample_stride_width // scale_w min_sizes = [ max(kernel_4, -(-kernel_5 // stride)) for kernel_4, kernel_5, stride in zip( self.config.decoder_stage_kernels[-1], self.config.decoder_stage5_kernel, upsample_stride, ) ] ghost_frames = decoder.trailing_pad_latent_frames * math.prod( upsample.stride[0] for upsample in decoder.upsamples[:-1] ) num_frames = features.shape[1] - ghost_frames height, width = features.shape[2:4] return LTX2VideoDiffusionTilePlan( temporal_tiles=tuple(_tile_intervals(num_frames, tile_t, stride_t, min_sizes[0])), height_tiles=tuple(_tile_intervals(height, tile_h, stride_h, min_sizes[1])), width_tiles=tuple(_tile_intervals(width, tile_w, stride_w, min_sizes[2])), num_frames=num_frames, height=height, width=width, scale_t=scale_t, scale_h=scale_h, scale_w=scale_w, stride_t=stride_t, stride_h=stride_h, stride_w=stride_w, blend_frames=(tile_t - stride_t) * scale_t, blend_height=(tile_h - stride_h) * scale_h, blend_width=(tile_w - stride_w) * scale_w, single_step_x0=num_inference_steps == 1 and decoder.model_output_type == "x0", ) @staticmethod def _iter_tiled_decode_coords(plan: LTX2VideoDiffusionTilePlan): for t_idx in range(len(plan.temporal_tiles)): for h_idx in range(len(plan.height_tiles)): for w_idx in range(len(plan.width_tiles)): yield t_idx, h_idx, w_idx @staticmethod def _get_tiled_feature_slice( features: torch.Tensor, coord: tuple[int, int, int], plan: LTX2VideoDiffusionTilePlan, ) -> tuple[torch.Tensor, bool, bool]: t_idx, h_idx, w_idx = coord t0, t1 = plan.temporal_tiles[t_idx] h0, h1 = plan.height_tiles[h_idx] w0, w1 = plan.width_tiles[w_idx] is_origin = t0 == 0 is_trailing = t1 == plan.num_frames feature_t1 = features.shape[1] if is_trailing else t1 return features[:, t0:feature_t1, h0:h1, w0:w1], is_origin, is_trailing def _tiled_pixel_shape_from_features( self, feature_tile: torch.Tensor, *, drop_leading_frame: bool, crop_trailing_ghost: bool, ) -> tuple[int, int, int, int, int]: """Predict one output tile's shape without materializing stage 4.""" decoder = self.decoder stride_t, stride_h, stride_w = decoder.upsamples[-1].stride num_frames = feature_tile.shape[1] * stride_t if stride_t == 2 and drop_leading_frame: num_frames -= 1 if crop_trailing_ghost and decoder.trailing_pad_latent_frames > 0: content_frames = max( num_frames - decoder.trailing_pad_latent_frames * decoder.temporal_compression_ratio, 1, ) num_frames = min(num_frames, max(content_frames, decoder.stage5_kernel[0])) return ( feature_tile.shape[0], decoder.out_channels, num_frames, feature_tile.shape[2] * stride_h * decoder.patch_size, feature_tile.shape[3] * stride_w * decoder.patch_size, ) @staticmethod def _slice_tiled_noise( x_t_full: torch.Tensor, coord: tuple[int, int, int], tile_pixel_shape: tuple[int, int, int, int, int], plan: LTX2VideoDiffusionTilePlan, ) -> torch.Tensor: t_idx, h_idx, w_idx = coord t0, _ = plan.temporal_tiles[t_idx] h0, _ = plan.height_tiles[h_idx] w0, _ = plan.width_tiles[w_idx] pixel_t0 = t0 * plan.scale_t - (1 if t0 != 0 and plan.scale_t == 2 else 0) return x_t_full[ :, :, pixel_t0 : pixel_t0 + tile_pixel_shape[2], h0 * plan.scale_h : h0 * plan.scale_h + tile_pixel_shape[3], w0 * plan.scale_w : w0 * plan.scale_w + tile_pixel_shape[4], ] def _merge_tiled_decode( self, tiles: dict[tuple[int, int, int], torch.Tensor], plan: LTX2VideoDiffusionTilePlan, ) -> torch.Tensor: """Blend distributed RGB tiles in Diffusers' serial order.""" frame_groups = [] for t_idx in range(len(plan.temporal_tiles)): rows = [ [tiles[(t_idx, h_idx, w_idx)] for w_idx in range(len(plan.width_tiles))] for h_idx in range(len(plan.height_tiles)) ] result_rows = [] for h_idx, row in enumerate(rows): result_row = [] for w_idx, tile in enumerate(row): if h_idx > 0: tile = self.blend_v(rows[h_idx - 1][w_idx], tile, plan.blend_height) if w_idx > 0: tile = self.blend_h(row[w_idx - 1], tile, plan.blend_width) keep_height = plan.stride_h * plan.scale_h if h_idx < len(rows) - 1 else tile.shape[3] keep_width = plan.stride_w * plan.scale_w if w_idx < len(row) - 1 else tile.shape[4] result_row.append(tile[:, :, :, :keep_height, :keep_width]) result_rows.append(torch.cat(result_row, dim=4)) frame_groups.append(torch.cat(result_rows, dim=3)) result = [] for t_idx, group in enumerate(frame_groups): if t_idx > 0: group = self.blend_t(frame_groups[t_idx - 1], group, plan.blend_frames) if t_idx < len(frame_groups) - 1: keep_frames = plan.stride_t * plan.scale_t - (1 if t_idx == 0 and plan.scale_t == 2 else 0) group = group[:, :, :keep_frames] result.append(group) return torch.cat(result, dim=2) def _default_generator(self, device: torch.device) -> torch.Generator: if device.type == "cpu": return torch.default_generator device_module = getattr(torch, device.type, None) default_generators = getattr(device_module, "default_generators", None) if default_generators is None: raise ValueError( f"Distributed LTX-2.5 diffusion decode on {device.type!r} requires an explicit torch.Generator." ) device_index = device.index if device_index is None: current_device = getattr(device_module, "current_device", None) device_index = current_device() if current_device is not None else 0 return default_generators[device_index] def _sync_generators( self, generator: torch.Generator | list[torch.Generator] | None, device: torch.device, ) -> torch.Generator | list[torch.Generator]: """Make every rank start tile-noise generation from rank 0's state.""" generators = self._default_generator(device) if generator is None else generator generator_list = generators if isinstance(generators, list) else [generators] for item in generator_list: state_on_device = item.get_state().to(device=device) dist.broadcast(state_on_device, src=0, group=self.distributed_executor.group) item.set_state(state_on_device.cpu()) return generators @staticmethod def _clone_generator(generator: torch.Generator) -> torch.Generator: cloned = torch.Generator(device=generator.device) cloned.set_state(generator.get_state()) return cloned def _clone_generators( self, generators: torch.Generator | list[torch.Generator], ) -> torch.Generator | list[torch.Generator]: if isinstance(generators, list): return [self._clone_generator(item) for item in generators] return self._clone_generator(generators) def _distributed_tile_split( self, z: torch.Tensor, generator: torch.Generator | list[torch.Generator] | None, num_inference_steps: int, ) -> tuple[list[LTX2VideoDiffusionTileTask], GridSpec]: decoder = self.decoder features = decoder.forward_stages_1_to_3(z) plan = self._build_tiled_decode_plan(features, num_inference_steps) generators = self._sync_generators(generator, z.device) x_t_full = None if not plan.single_step_x0: pixel_frames = plan.num_frames * plan.scale_t - (1 if plan.scale_t == 2 else 0) x_t_full = randn_tensor( ( z.shape[0], decoder.out_channels, pixel_frames, plan.height * plan.scale_h, plan.width * plan.scale_w, ), generator=generators, device=z.device, dtype=z.dtype, ) tasks = [] for tile_id, coord in enumerate(self._iter_tiled_decode_coords(plan)): feature_tile, is_origin, is_trailing = self._get_tiled_feature_slice(features, coord, plan) tile_pixel_shape = self._tiled_pixel_shape_from_features( feature_tile, drop_leading_frame=is_origin, crop_trailing_ghost=is_trailing, ) tile_generator = None x_t = None if plan.single_step_x0: # Save the canonical serial state for this tile, then advance # the request generator exactly as the single-rank tiled path # does. Task placement therefore cannot change tile noise. tile_generator = self._clone_generators(generators) discarded_noise = randn_tensor( tile_pixel_shape, generator=generators, device=z.device, dtype=z.dtype, ) del discarded_noise else: x_t = self._slice_tiled_noise(x_t_full, coord, tile_pixel_shape, plan) tasks.append( LTX2VideoDiffusionTileTask( tile_id=tile_id, grid_coord=coord, tensor=feature_tile, workload=math.prod(tile_pixel_shape), drop_leading_frame=is_origin, crop_trailing_ghost=is_trailing, noise_generator=tile_generator, x_t=x_t, ) ) return tasks, GridSpec( split_dims=(2, 3, 4), grid_shape=(len(plan.temporal_tiles), len(plan.height_tiles), len(plan.width_tiles)), tile_spec={"plan": plan}, output_dtype=z.dtype, ) def _distributed_tile_exec( self, task: LTX2VideoDiffusionTileTask, num_inference_steps: int, ) -> torch.Tensor: context = self.decoder.forward_stage_4( task.tensor, drop_leading_frame=task.drop_leading_frame, crop_trailing_ghost=task.crop_trailing_ghost, ) tile_pixel_shape = ( context.shape[0], self.decoder.out_channels, context.shape[1], context.shape[2] * self.decoder.patch_size, context.shape[3] * self.decoder.patch_size, ) if task.x_t is None: x_t = randn_tensor( tile_pixel_shape, generator=task.noise_generator, device=context.device, dtype=context.dtype, ) else: x_t = task.x_t return self.decoder.denoise(context, x_t, num_inference_steps) def _distributed_tile_merge( self, tiles: dict[tuple[int, int, int], torch.Tensor], grid_spec: GridSpec, ) -> torch.Tensor: plan = grid_spec.tile_spec["plan"] if not isinstance(plan, LTX2VideoDiffusionTilePlan): raise TypeError(f"Expected an LTX2VideoDiffusionTilePlan, got {type(plan)!r}.") return self._merge_tiled_decode(tiles, plan) def tiled_decode( self, z: torch.Tensor, generator: torch.Generator | list[torch.Generator] | None = None, num_inference_steps: int | None = None, ) -> torch.Tensor: if not self.is_distributed_enabled(): return super().tiled_decode(z, generator=generator, num_inference_steps=num_inference_steps) logger.debug("LTX-2.5 diffusion decoder running with distributed stage-4/stage-5 tiles") num_inference_steps = num_inference_steps or self.decoder.default_num_inference_steps result = self.distributed_executor.execute( z, DistributedOperator( split=lambda tensor: self._distributed_tile_split(tensor, generator, num_inference_steps), exec=lambda task: self._distributed_tile_exec(task, num_inference_steps), merge=self._distributed_tile_merge, ), broadcast_result=False, ) if result.numel() == 0: # The base decode method crops five dimensions before the LTX # runtime discards non-output-rank results. return torch.empty((0, self.decoder.out_channels, 0, 0, 0), device=z.device, dtype=z.dtype) return result