--- toc_depth: 4 --- # Diffusion Module Architecture Design The vLLM-Omni diffusion module (`vllm_omni/diffusion`) is a high-performance inference engine for diffusion models, designed with a modular architecture that separates concerns across multiple components. It provides efficient execution for non-autoregressive generation tasks such as image and video generation. This document describes the architecture design of the diffusion module, including the diffusion engine, scheduler, worker, diffusion pipeline, and acceleration components.
Main Components of the Diffusion Module
**Table of Content:** - [Architecture Overview](#architecture-overview) - [Diffusion Engine](#1-diffusion-engine) - [Scheduler](#2-scheduler) - [Worker](#3-worker) - [Diffusion Pipeline](#4-diffusion-pipeline) - [Acceleration Components](#5-acceleration-components) - [Attention Backends](#51-attention-backends) - [Parallel Attention](#52-parallel-attention) - [Cache Backends](#53-cache-backends) - [Parallel Strategies](#54-parallel-strategies) - [Data Flow](#6-data-flow) --- ## Architecture Overview The diffusion module follows a **multi-process, distributed architecture** with clear separation of concerns:
Diffusion Architecture Overview
--- ## 1. Diffusion Engine **Location**: `vllm_omni/diffusion/diffusion_engine.py` ### Responsibilities The `DiffusionEngine` is the **orchestrator** of the diffusion inference system. It manages the lifecycle of worker processes and coordinates the execution flow. ### Key Components #### 1.1 Initialization ```python class DiffusionEngine: def __init__(self, od_config: OmniDiffusionConfig): self.od_config = od_config self.post_process_func = get_diffusion_post_process_func(od_config) self.pre_process_func = get_diffusion_pre_process_func(od_config) self._processes: list[mp.Process] = [] self._make_client() ``` **Key Features**: - **Pre/Post Processing**: Registers model-specific pre-processing and post-processing functions via registry pattern - **Worker Management**: Launches and manages multiple worker processes (one per GPU) - **Process Isolation**: Uses multiprocessing for true parallelism #### 1.2 Worker Launch Process The engine launches workers using a **spawn** method: ```python def _launch_workers(self, broadcast_handle): # Creates one process per GPU for i in range(num_gpus): process = mp.Process( target=worker_proc.worker_main, args=(i, od_config, writer, broadcast_handle), name=f"DiffusionWorker-{i}", ) process.start() ``` **Design Decisions**: - **Spawn Method**: Ensures clean state for each worker (no shared memory issues) - **Pipe Communication**: Uses `mp.Pipe` for initialization handshake - **Device Selection**: Each worker is assigned a specific GPU (`cuda:{rank}`) #### 1.3 Request Processing Flow ```python def step(self, requests: list[OmniDiffusionRequest]): # 1. Pre-process requests requests = self.pre_process_func(requests) # 2. Send to scheduler and wait for response output = self.add_req_and_wait_for_response(requests) # 3. Post-process results result = self.post_process_func(output.output) return result ``` **Flow**: 1. **Pre-processing**: Applies model-specific transformations 2. **Scheduling**: Delegates to scheduler for distribution 3. **Post-processing**: Converts raw outputs to final format (e.g., PIL images) --- ## 2. Scheduler **Location**: `vllm_omni/diffusion/sched/` ### Architecture The scheduler is a **request-state scheduler**. It owns request lifecycle management and scheduling decisions, while execution stays in `DiffusionEngine` and the executor. ### Key Components #### 2.1 Scheduler Interface ```python class SchedulerInterface(ABC): def add_request(self, request: OmniDiffusionRequest) -> str: ... def schedule(self) -> DiffusionSchedulerOutput: ... def update_from_output( self, sched_output: DiffusionSchedulerOutput, output: DiffusionOutput, ) -> set[str]: ... ``` **Responsibilities**: - **Lifecycle contract**: Defines how the engine adds requests, triggers one scheduling cycle, and feeds executor results back. - **Stable boundary**: `DiffusionSchedulerOutput` is the only scheduling result consumed by `DiffusionEngine`. - **Pluggability**: Different scheduler policies can reuse the same engine integration path. #### 2.2 Request State Model ```python class DiffusionRequestStatus(enum.IntEnum): WAITING = ... RUNNING = ... PREEMPTED = ... FINISHED_COMPLETED = ... FINISHED_ABORTED = ... FINISHED_ERROR = ... @dataclass class DiffusionRequestState: request_id: str req: OmniDiffusionRequest status: DiffusionRequestStatus = DiffusionRequestStatus.WAITING ``` **Design Features**: - **Explicit lifecycle**: Requests move through waiting, running, optional preemption, and terminal states. - **Centralized error handling**: Completion, abort, and error states are all normalized in the scheduler layer. #### 2.3 Shared Bookkeeping in `_BaseScheduler` ```python class _BaseScheduler(SchedulerInterface): def __init__(self) -> None: self._request_states = {} self._waiting = deque() self._running = [] self._finished_req_ids = set() self.max_num_running_reqs = 1 ``` **Design Features**: - **Common state storage**: Shared request maps and waiting/running sets live in the base class. - **Shared cleanup logic**: Duplicate-request checks, finish handling, and state removal are centralized instead of duplicated in each policy. - **Current constraint boundary**: `_BaseScheduler` derives `max_num_running_reqs` from `max_num_seqs`. Request-mode diffusion can use that capacity for compatible independent requests when the configured pipeline declares `supports_request_batch = True`; step-wise diffusion uses the same scheduler capacity for compatible step batches. #### 2.4 Current `RequestScheduler` Policy ```python class RequestScheduler(_BaseScheduler): def schedule(self) -> DiffusionSchedulerOutput: # 1. keep existing RUNNING requests in the scheduling result # 2. pull WAITING requests while capacity remains # 3. move newly admitted requests into RUNNING ``` **Behavior**: - **FIFO request scheduling**: Waiting requests are promoted in queue order. - **Compatible request admission**: `RequestScheduler` admits waiting requests while capacity remains and the request's `SamplingParamsKey` is compatible with the active batch. Request-mode execution keeps each logical request independent, while batch-capable pipelines receive the scheduled requests as a runner-side `DiffusionRequestBatch`. - **Executor result feedback**: `update_from_output()` converts executor output into `FINISHED_COMPLETED` or `FINISHED_ERROR` and returns finished request ids. #### 2.5 Engine-Driven Execution Loop ```python request_id = scheduler.add_request(request) while True: sched_output = scheduler.schedule() output = executor.add_req(req) finished_req_ids = scheduler.update_from_output(sched_output, output) ``` **Design Decisions**: - **Separation of concerns**: Scheduler manages state and policy; executor handles runtime execution. - **No scheduler-owned IPC**: Scheduler no longer talks to workers directly. - **Split concurrency model**: Request-mode diffusion can schedule a static batch of compatible independent requests for one full pipeline forward when the pipeline supports request-level batching; the step-wise path can also admit or remove compatible requests between denoise steps. See [Request-Level Batching](../feature/diffusion_request_level_batching.md) and [Continuous Batching for Step-Wise Diffusion](../feature/diffusion_continuous_batching.md). --- ## 3. Worker **Location**: `vllm_omni/diffusion/worker/gpu_worker.py` ### Architecture Workers are **independent processes** that execute the actual model inference. Each worker runs on a dedicated GPU and participates in distributed inference. ### Key Components #### 3.1 Worker Process Structure ```python class WorkerProc: def __init__(self, od_config, gpu_id, broadcast_handle): # Initialize ZMQ context for IPC self.context = zmq.Context(io_threads=2) # Connect to broadcast queue (receive requests) self.mq = MessageQueue.create_from_handle(broadcast_handle, gpu_id) # Create result queue (only rank 0) if gpu_id == 0: self.result_mq = MessageQueue(n_reader=1, ...) # Initialize GPU worker self.worker = GPUWorker(local_rank=gpu_id, rank=gpu_id, od_config=od_config) ``` **Initialization Steps**: 1. **IPC Setup**: Creates ZMQ context and message queues 2. **Distributed Environment Setup**: Initializes PyTorch distributed communication - For CUDA GPUs: Uses NCCL (fast GPU communication) - For NPU: Uses HCCL (Huawei Collective Communications Library) - For other devices: Uses appropriate backend (GLOO, MCCL, etc.) 3. **Model Loading**: Loads diffusion pipeline on assigned GPU 4. **Cache Setup**: Enables cache backend if configured. #### 3.2 GPU Worker ```python class GPUWorker: def init_device_and_model(self): # Set distributed environment variables os.environ["RANK"] = str(rank) os.environ["WORLD_SIZE"] = str(world_size) # Initialize PyTorch distributed init_distributed_environment(world_size, rank) parallel_config = self.od_config.parallel_config initialize_model_parallel( data_parallel_size=parallel_config.data_parallel_size, cfg_parallel_size=parallel_config.cfg_parallel_size, sequence_parallel_size=parallel_config.sequence_parallel_size, tensor_parallel_size=parallel_config.tensor_parallel_size, pipeline_parallel_size=parallel_config.pipeline_parallel_size, ) # Load model model_loader = DiffusersPipelineLoader(load_config, self.od_config) self.pipeline = model_loader.load_model(load_device=f"cuda:{rank}") # Setup cache backend from vllm_omni.diffusion.cache.selector import get_cache_backend self.cache_backend = get_cache_backend(od_config.cache_backend, od_config.cache_config) if self.cache_backend is not None: self.cache_backend.enable(self.pipeline) ``` **Key Features**: - **Tensor Parallelism**: Supports multi-GPU tensor parallelism via PyTorch distributed - **Model Loading**: Uses `DiffusersPipelineLoader` for efficient weight loading - **Cache Integration**: Enables cache backends (TeaCache, cache-dit, etc.) transparently #### 3.3 Worker Busy Loop ```python def worker_busy_loop(self): while self._running: # 1. Receive unified message (generation request, RPC request, or shutdown) msg = self.recv_message() # 2. Route message based on type if isinstance(msg, dict) and msg.get("type") == "rpc": # Handle RPC request result, should_reply = self.execute_rpc(msg) if should_reply: self.return_result(result) elif isinstance(msg, dict) and msg.get("type") == "shutdown": # Handle shutdown message self._running = False else: # Handle generation request (OmniDiffusionRequest list) output = self.worker.execute_model(msg, self.od_config) self.return_result(output) ``` **Execution Flow**: 1. **Receive**: Dequeues unified messages from shared memory queue 2. **Route**: Handles different message types (generation, RPC, shutdown) 3. **Execute**: Runs forward pass through pipeline for generation requests 4. **Respond**: Sends results back (rank 0 for generation, specified rank for RPC) #### 3.4 Model Execution ```python @torch.inference_mode() def execute_model(self, reqs: list[OmniDiffusionRequest], od_config): req = reqs[0] # TODO: support batching # Refresh cache backend if enabled if self.cache_backend is not None and self.cache_backend.is_enabled(): self.cache_backend.refresh(self.pipeline, req.num_inference_steps) # Set forward context for parallelism with set_forward_context( vllm_config=self.vllm_config, omni_diffusion_config=self.od_config ): output = self.pipeline.forward(req) return output ``` The model execution leverages multiple parallelism strategies that are transparently applied during the forward pass. The `set_forward_context()` context manager makes parallel group information available throughout the forward pass: ```python # Inside transformer layers, parallel groups are accessed via: from vllm_omni.diffusion.distributed.parallel_state import ( get_sp_group, get_dp_group, get_cfg_group, get_pp_group ) ``` **Optimizations**: - **Cache Refresh**: Clears cache state before each generation for clean state - **Context Management**: Forward context ensures parallel groups are available during execution - **Single Request**: Currently processes one request at a time (batching TODO) --- ## 4. Diffusion Pipeline **Location**: `vllm_omni/diffusion/models/*/pipeline_*.py` The pipeline is the **model-specific implementation** that orchestrates the diffusion process. Different models (QwenImage, Wan2.2, Z-Image) have their own pipeline implementations. Most pipeline implementation are referred from `diffusers`. The multi-step diffusion loop is usually the most time-consuming part during the overall inference process, which is defined by the `diffuse` function in the pipeline class. An example is as follows: ```python def diffuse(self, ...): for i, t in enumerate(timesteps): # Forward pass for positive prompt transformer_kwargs = { "hidden_states": latents, "timestep": timestep / 1000, "encoder_hidden_states": prompt_embeds, } noise_pred = self.transformer(**transformer_kwargs)[0] # Forward pass for negative prompt (CFG) if do_true_cfg: neg_transformer_kwargs = {...} neg_transformer_kwargs["cache_branch"] = "negative" neg_noise_pred = self.transformer(**neg_transformer_kwargs)[0] # Combine predictions comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred) noise_pred = comb_pred * (cond_norm / noise_norm) # Scheduler step latents = self.scheduler.step(noise_pred, t, latents)[0] return latents ``` **Key Features**: - **CFG Support**: Handles classifier-free guidance with separate forward passes - **Cache Branching**: Uses `cache_branch` parameter for cache-aware execution - **True CFG**: Implements advanced CFG with norm preservation To learn more about the diffusion pipeline and how to add a new diffusion pipeline, please view [Adding Diffusion Model](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/model/adding_diffusion_model) --- ## 5. Acceleration Components ### 5.1 Attention Backends **Location**: `vllm_omni/diffusion/attention/` #### Architecture The attention system uses a **role-aware backend selector pattern**. Each `Attention` site declares a semantic `role` (`"self"`, `"cross"`, or a model-specific string). The selector consults the user's `AttentionConfig` to pick a backend per role and falls back to the platform default when nothing is configured. #### Backend Selection **Location**: `vllm_omni/diffusion/attention/selector.py` ```python class Attention(nn.Module): def __init__(self, num_heads, head_size, causal, softmax_scale, *, role="self", role_category=None, ...): # Resolve backend for this role from the active OmniDiffusionConfig config = get_current_diffusion_config_or_none() attention_config = config.diffusion_attention_config if config is not None else None attn_backend_cls, spec = get_attn_backend_for_role( role=role, head_size=head_size, attention_config=attention_config, role_category=role_category, ) self.attn_impl_cls = attn_backend_cls.get_impl_cls() self.attention = self.attn_impl_cls(..., backend_kwargs=spec.extra if spec else None) ``` **Available Backends**: - **FlashAttention** (`FLASH_ATTN`): Memory-efficient kernel that dispatches to FA2/FA3 on CUDA, AITER on ROCm, and `mindiesd` on Ascend NPU. - **SDPA** (`TORCH_SDPA`): PyTorch's scaled dot-product attention — cross-platform fallback. - **SageAttention** (`SAGE_ATTN`): Quantized attention implementation from the SageAttention library. These backends provide the **kernel implementations** for attention computation. For attention-level sequence parallelism strategies (Ring Attention, Ulysses), see [Parallel Attention](#52-parallel-attention). #### Backend Selection Mechanism Selection is driven by `OmniDiffusionConfig.diffusion_attention_config`, which carries a global `default` plus a `per_role` map of `AttentionSpec` entries. A role string is resolved in this order: ```python def get_attn_backend_for_role(role, head_size, attention_config=None, role_category=None): # 1. attention_config.per_role[role] — exact match # 2. attention_config.per_role[role_category] — category fallback # 3. attention_config.default — global default # 4. Platform default — hardware-specific if attention_config is not None: spec, source = attention_config.resolve_with_source( role=role, role_category=role_category, ) if spec is not None: return load_backend(spec.backend), spec return current_omni_platform.get_diffusion_attn_backend_cls(...), None ``` **Selection Priority**: 1. **Per-role override** (`--diffusion-attention-config.per_role.
End-to-end Data Flow in the vLLM-Omni Diffusion Module
``` 1. User Request └─> Omni.generate(prompt) └─> Prepare OmniDiffusionRequest └─> DiffusionEngine.step(requests) 2. Pre-processing └─> pre_process_func(requests) └─> Model-specific transformations 3. Scheduling └─> scheduler.add_request(request) └─> scheduler.schedule() └─> DiffusionEngine submits scheduled request to executor.add_req(req) 4. Worker Execution └─> WorkerProc.worker_busy_loop() └─> GPUWorker.execute_model(reqs) └─> Pipeline.forward(req) ├─> encode_prompt() ├─> prepare_latents() ├─> diffuse() [loop] │ ├─> transformer.forward() [with cache backend hooks] │ └─> scheduler.step() └─> vae.decode() 5. Result Collection └─> Executor returns DiffusionOutput └─> scheduler.update_from_output(...) └─> DiffusionEngine pops finished request state 6. Post-processing └─> post_process_func(output) └─> Convert to PIL images / final format ``` ---