# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Callable from concurrent.futures import Future from dataclasses import replace from typing import TYPE_CHECKING, Any from vllm.utils.import_utils import resolve_obj_by_qualname from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheSpec from vllm.v1.outputs import KVConnectorOutput from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig from vllm_omni.diffusion.sched.interface import CachedRequestData if TYPE_CHECKING: from vllm_omni.diffusion.request import OmniDiffusionRequest from vllm_omni.diffusion.sched.interface import DiffusionSchedulerOutput from vllm_omni.diffusion.worker.utils import BaseRunnerOutput class DiffusionExecutor(ABC): """Abstract base class for Diffusion executors.""" uses_multiproc: bool = False @staticmethod def get_class(od_config: OmniDiffusionConfig) -> type[DiffusionExecutor]: executor_class: type[DiffusionExecutor] distributed_executor_backend = od_config.distributed_executor_backend # Mirror vLLM's `world_size == 1 -> "uni"` default # (vllm/config/parallel.py). A single-GPU diffusion deployment has # nothing to distribute: spawning a worker only adds MessageQueues, # /dev/shm output segments, and a second model load. Explicit "mp" # keeps process isolation and RPC timeouts. Multi-GPU still defaults # to "mp". if distributed_executor_backend is None: num_gpus = od_config.num_gpus or 1 distributed_executor_backend = "uni" if num_gpus == 1 else "mp" if isinstance(distributed_executor_backend, type): if not issubclass(distributed_executor_backend, DiffusionExecutor): raise TypeError( "distributed_executor_backend must be a subclass of " f"DiffusionExecutor. Got {distributed_executor_backend}." ) executor_class = distributed_executor_backend elif distributed_executor_backend == "ray": raise NotImplementedError("ray backend is not yet supported.") elif distributed_executor_backend == "mp": from vllm_omni.diffusion.executor.multiproc_executor import MultiprocDiffusionExecutor executor_class = MultiprocDiffusionExecutor elif distributed_executor_backend == "uni": from vllm_omni.diffusion.executor.uniproc_executor import UniProcDiffusionExecutor executor_class = UniProcDiffusionExecutor elif distributed_executor_backend == "external_launcher": raise NotImplementedError("external_launcher backend is not yet supported.") elif isinstance(distributed_executor_backend, str): try: executor_class = resolve_obj_by_qualname(distributed_executor_backend) except (ImportError, ValueError) as e: raise ValueError( f"Failed to load executor backend '{distributed_executor_backend}'. " f"Ensure it is a valid python path. Error: {e}" ) from e if not issubclass(executor_class, DiffusionExecutor): raise TypeError( f"distributed_executor_backend must be a subclass of DiffusionExecutor. Got {executor_class}." ) else: raise ValueError(f"Unknown distributed executor backend: {distributed_executor_backend}") return executor_class def __init__(self, od_config: OmniDiffusionConfig) -> None: self.od_config = od_config self._init_executor() @abstractmethod def _init_executor(self) -> None: """Initialize the executor (e.g., launch workers, setup IPC).""" pass @property @abstractmethod def is_dead(self) -> bool: """Whether the executor is shut down or has failed fatally.""" pass @abstractmethod def execute_request(self, scheduler_output: DiffusionSchedulerOutput) -> BaseRunnerOutput: """Execute request-mode work from a scheduler output.""" pass @abstractmethod def execute_batch(self, scheduler_output: DiffusionSchedulerOutput) -> BaseRunnerOutput: """Execute request-mode work through the request-batch path.""" pass @abstractmethod def execute_step(self, scheduler_output: DiffusionSchedulerOutput) -> BaseRunnerOutput: """Execute step-mode work from a scheduler output.""" pass @abstractmethod def collective_rpc( self, method: str, timeout: float | None = None, args: tuple = (), kwargs: dict | None = None, unique_reply_rank: int | None = None, exec_all_ranks: bool = False, ) -> Any: """Execute a method on workers.""" pass @abstractmethod def check_health(self) -> None: """Check if the executor and workers are healthy.""" pass def register_failure_callback(self, callback: Callable[[], None]) -> None: """Register a callback invoked when the executor fatally fails. Executors without a background failure monitor can keep the default no-op implementation. """ return None def wait_output_ready(self, async_output_id: str) -> Future[DiffusionOutput]: """Resolve deferred output; only asynchronous executors implement this.""" raise NotImplementedError(f"{type(self).__name__} does not support asynchronous output") def get_kv_cache_specs(self) -> list[dict[str, KVCacheSpec]]: """Collect rank-local native specs after every Worker loads its model.""" result = self.collective_rpc( "get_kv_cache_specs", unique_reply_rank=0, exec_all_ranks=True, ) if not isinstance(result, list): raise TypeError(f"get_kv_cache_specs returned {type(result).__name__}, expected list") return result def determine_available_kv_memory(self, profile_requests: list[OmniDiffusionRequest]) -> list[int]: """Profile and collect the KV memory budget on every Worker rank.""" result = self.collective_rpc( "determine_available_kv_memory", args=(profile_requests,), unique_reply_rank=0, exec_all_ranks=True, ) if not isinstance(result, list) or not all(isinstance(value, int) for value in result): raise TypeError("determine_available_kv_memory must return list[int]") return result def set_kv_cache_configs(self, kv_cache_configs: list[KVCacheConfig], resolved_max_model_len: int) -> None: """Send rank-local configs and the resolved model length to all Workers.""" # The default control-plane RPC mode executes on every rank and has # rank 0 return the gathered rank statuses, so failures on nonzero # ranks are not silently dropped. self.collective_rpc("set_kv_cache_configs", args=(kv_cache_configs, resolved_max_model_len)) def remove_diffusion_kv_requests(self, request_ids: list[str | tuple[str, int]]) -> None: """Clear request rows on every Worker after Scheduler retirement.""" unique_request_ids = list(dict.fromkeys(request_ids)) if not unique_request_ids: return self.collective_rpc( "remove_diffusion_kv_requests", args=(unique_request_ids,), ) def prepare_kv_for_forward(self, scheduler_output: DiffusionSchedulerOutput) -> KVConnectorOutput | None: if scheduler_output.kv_prefetch_connector_metadata is not None: current = replace( scheduler_output, kv_transfer_request_ids=scheduler_output.kv_transfer_request_ids - scheduler_output.kv_prefetch_request_ids, kv_prefetch_connector_metadata=None, kv_prefetch_request_ids=set(), ) # First complete the current request on every rank. Only then # submit B, preventing Mooncake from coalescing its bytes with A. self.prepare_kv_for_forward(current) return self.prepare_kv_for_forward( replace( current, kv_connector_metadata=scheduler_output.kv_prefetch_connector_metadata, kv_transfer_request_ids=scheduler_output.kv_prefetch_request_ids, kv_required_request_ids=set(), kv_finished_request_ids=set(), ) ) if ( scheduler_output.kv_connector_metadata is None and not scheduler_output.kv_required_request_ids and not scheduler_output.kv_poll_only ): return None transfer_output = replace( scheduler_output, scheduled_new_reqs=[], scheduled_cached_reqs=CachedRequestData.make_empty(), kv_prefetch_job=None, ) outputs: list[KVConnectorOutput] = self.collective_rpc( "prepare_kv_for_forward", args=(transfer_output,), unique_reply_rank=0, exec_all_ranks=True, ) if len(outputs) != self.od_config.num_gpus or any(output.invalid_block_ids for output in outputs): # Missing ranks / invalid pages cannot establish safe ownership. raise RuntimeError("Diffusion KV receive failed on one or more ranks") if scheduler_output.kv_required_request_ids is not None: # Prefetch workers retain completion events until retirement. # Their cumulative snapshots can be intersected directly. finished = set.intersection(*(set(output.finished_recving or ()) for output in outputs)) if not scheduler_output.kv_required_request_ids.issubset(finished): raise RuntimeError("Required diffusion KV receive did not complete on every rank") outputs[0].finished_recving = finished return outputs[0] completed = getattr(self, "_kv_receive_completed_ranks", {}) for rank, output in enumerate(outputs): for request_id in output.finished_recving or (): completed.setdefault(request_id, set()).add(rank) finished = {request_id for request_id, ranks in completed.items() if len(ranks) == len(outputs)} for request_id in finished: del completed[request_id] self._kv_receive_completed_ranks = completed outputs[0].finished_recving = finished return outputs[0] @abstractmethod def shutdown(self) -> None: """Shutdown the executor and release resources.""" pass