# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import os import threading import time from collections.abc import Iterable from typing import Any import numpy as np import PIL.Image import torch from vllm.logger import init_logger from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig from vllm_omni.diffusion.executor.abstract import DiffusionExecutor from vllm_omni.diffusion.registry import ( DiffusionModelRegistry, get_diffusion_post_process_func, get_diffusion_pre_process_func, ) from vllm_omni.diffusion.request import OmniDiffusionRequest from vllm_omni.diffusion.sched import RequestScheduler, SchedulerInterface from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt from vllm_omni.outputs import OmniRequestOutput logger = init_logger(__name__) def supports_image_input(model_class_name: str) -> bool: model_cls = DiffusionModelRegistry._try_load_model_cls(model_class_name) if model_cls is None: return False return bool(getattr(model_cls, "support_image_input", False)) def supports_audio_input(model_class_name: str) -> bool: model_cls = DiffusionModelRegistry._try_load_model_cls(model_class_name) if model_cls is None: return False return bool(getattr(model_cls, "support_audio_input", False)) def image_color_format(model_class_name: str) -> str: model_cls = DiffusionModelRegistry._try_load_model_cls(model_class_name) return getattr(model_cls, "color_format", "RGB") def supports_audio_output(model_class_name: str) -> bool: model_cls = DiffusionModelRegistry._try_load_model_cls(model_class_name) if model_cls is None: return False return bool(getattr(model_cls, "support_audio_output", False)) class DiffusionEngine: """The diffusion engine for vLLM-Omni diffusion models.""" def __init__( self, od_config: OmniDiffusionConfig, scheduler: SchedulerInterface | None = None, ): """Initialize the diffusion engine. Args: config: The configuration for the diffusion engine. """ 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) executor_class = DiffusionExecutor.get_class(od_config) self.executor = executor_class(od_config) self.scheduler: SchedulerInterface = scheduler or RequestScheduler() self.scheduler.initialize(od_config) self._rpc_lock = threading.Lock() try: self._dummy_run() except Exception as e: logger.error(f"Dummy run failed: {e}") self.close() raise e def step(self, request: OmniDiffusionRequest) -> list[OmniRequestOutput]: diffusion_engine_start_time = time.perf_counter() # Apply pre-processing if available preprocess_time = 0.0 if self.pre_process_func is not None: preprocess_start_time = time.perf_counter() request = self.pre_process_func(request) preprocess_time = time.perf_counter() - preprocess_start_time logger.info(f"Pre-processing completed in {preprocess_time:.4f} seconds") exec_start_time = time.perf_counter() output = self.add_req_and_wait_for_response(request) exec_total_time = time.perf_counter() - exec_start_time if output.error: raise Exception(f"{output.error}") logger.info("Generation completed successfully.") if output.output is None: logger.warning("Output is None, returning empty OmniRequestOutput") return [ OmniRequestOutput.from_diffusion( request_id=request.request_ids[i] if i < len(request.request_ids) else "", images=[], prompt=prompt, metrics={}, latents=None, ) for i, prompt in enumerate(request.prompts) ] # When CPU offload is enabled, move output to CPU before # post-processing to avoid device OOM — model weights may still # reside on the device and leave no headroom for intermediates. output_data = output.output if ( self.od_config.enable_cpu_offload and isinstance(output_data, torch.Tensor) and output_data.device.type != "cpu" ): output_data = output_data.cpu() postprocess_start_time = time.perf_counter() outputs = self.post_process_func(output_data) if self.post_process_func is not None else output_data audio_payload = None if isinstance(outputs, dict): audio_payload = outputs.get("audio") outputs = outputs.get("video", outputs) postprocess_time = time.perf_counter() - postprocess_start_time logger.info(f"Post-processing completed in {postprocess_time:.4f} seconds") step_total_ms = (time.perf_counter() - diffusion_engine_start_time) * 1000 logger.info( "DiffusionEngine.step breakdown: preprocess=%.2f ms, " "add_req_and_wait=%.2f ms, postprocess=%.2f ms, total=%.2f ms", preprocess_time * 1000, exec_total_time * 1000, postprocess_time * 1000, step_total_ms, ) # Convert to OmniRequestOutput format # Ensure outputs is a list if not isinstance(outputs, list): outputs = [outputs] if outputs is not None else [] metrics = { "preprocess_time_ms": preprocess_time * 1000, "diffusion_engine_exec_time_ms": (time.perf_counter() - diffusion_engine_start_time) * 1000, "diffusion_engine_total_time_ms": exec_total_time * 1000, "image_num": int(request.sampling_params.num_outputs_per_prompt), "resolution": int(request.sampling_params.resolution), "postprocess_time_ms": postprocess_time * 1000, } if self.pre_process_func is not None: metrics["preprocessing_time_ms"] = preprocess_time * 1000 # Handle single request or multiple requests if len(request.prompts) == 1: # Single request: return single OmniRequestOutput prompt = request.prompts[0] request_id = request.request_ids[0] if request.request_ids else "" if supports_audio_output(self.od_config.model_class_name): request_audio_payload = outputs[0] if len(outputs) == 1 else outputs return [ OmniRequestOutput.from_diffusion( request_id=request_id, images=[], prompt=prompt, metrics=metrics, latents=output.trajectory_latents, multimodal_output={"audio": request_audio_payload}, final_output_type="audio", stage_durations=output.stage_durations, ), ] else: mm_output = {} if audio_payload is not None: mm_output["audio"] = audio_payload return [ OmniRequestOutput.from_diffusion( request_id=request_id, images=outputs, prompt=prompt, metrics=metrics, latents=output.trajectory_latents, custom_output=output.custom_output or {}, multimodal_output=mm_output, stage_durations=output.stage_durations, ), ] else: # Multiple requests: return list of OmniRequestOutput # Split images based on num_outputs_per_prompt for each request results = [] output_idx = 0 for i, prompt in enumerate(request.prompts): request_id = request.request_ids[i] if i < len(request.request_ids) else "" # Get images for this request num_outputs = request.sampling_params.num_outputs_per_prompt start_idx = output_idx end_idx = start_idx + num_outputs request_outputs = outputs[start_idx:end_idx] if output_idx < len(outputs) else [] output_idx = end_idx if supports_audio_output(self.od_config.model_class_name): request_audio_payload = request_outputs[0] if len(request_outputs) == 1 else request_outputs results.append( OmniRequestOutput.from_diffusion( request_id=request_id, images=[], prompt=prompt, metrics=metrics, latents=output.trajectory_latents, multimodal_output={"audio": request_audio_payload}, final_output_type="audio", stage_durations=output.stage_durations, ), ) else: mm_output = {} if audio_payload is not None: sliced_audio = audio_payload if isinstance(audio_payload, (list, tuple)): sliced_audio = audio_payload[start_idx:end_idx] if len(sliced_audio) == 1: sliced_audio = sliced_audio[0] elif hasattr(audio_payload, "shape") and getattr(audio_payload, "shape", None) is not None: if len(audio_payload.shape) > 0 and audio_payload.shape[0] >= end_idx: sliced_audio = audio_payload[start_idx:end_idx] if num_outputs == 1: sliced_audio = sliced_audio[0] mm_output["audio"] = sliced_audio results.append( OmniRequestOutput.from_diffusion( request_id=request_id, images=request_outputs, prompt=prompt, metrics=metrics, latents=output.trajectory_latents, custom_output=output.custom_output or {}, multimodal_output=mm_output, stage_durations=output.stage_durations, ), ) return results @staticmethod def make_engine( config: OmniDiffusionConfig, scheduler: SchedulerInterface | None = None, ) -> "DiffusionEngine": """Factory method to create a DiffusionEngine instance. Args: config: The configuration for the diffusion engine. Returns: An instance of DiffusionEngine. """ return DiffusionEngine(config, scheduler=scheduler) def add_req_and_wait_for_response(self, request: OmniDiffusionRequest) -> DiffusionOutput: with self._rpc_lock: target_sched_req_id = self.scheduler.add_request(request) # keep scheduling and executing until the target request is finished while True: sched_output = self.scheduler.schedule() if sched_output.is_empty: if not self.scheduler.has_requests(): raise RuntimeError("Diffusion scheduler has no runnable requests.") continue # NOTE: add_req_and_wait_for_response() is synchronous, and # the scheduler currently enforces _max_batch_size = 1 (see # vllm_omni/diffusion/sched/base_scheduler.py), so we directly # take the single scheduled request here. sched_req_id = sched_output.scheduled_req_ids[0] req = sched_output.scheduled_new_reqs[0].req try: output = self.executor.add_req(req) except Exception as exc: logger.error( "Execution failed for diffusion request %s", sched_req_id, exc_info=True, ) output = DiffusionOutput(error=str(exc)) finished_req_ids = self.scheduler.update_from_output(sched_output, output) if target_sched_req_id in finished_req_ids: self.scheduler.pop_request_state(target_sched_req_id) return output def start_profile(self, trace_filename: str | None = None) -> None: """ Start torch profiling on all diffusion workers. Creates a directory (if needed) and sets up a base filename template for per-rank profiler traces (typically saved as