Files
vllm-omni/vllm_omni/diffusion/worker/utils.py
T

163 lines
5.6 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Per-request mutable state for step-wise diffusion execution."""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
import torch
if TYPE_CHECKING:
from vllm_omni.diffusion.data import DiffusionOutput
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniPromptType
@dataclass
class DiffusionRequestState:
"""Per-request mutable state across all pipeline stages.
Owned by Runner and passed through all step-execution stages:
``prepare_encode()`` initializes/updates fields, ``denoise_step()`` and
``step_scheduler()`` mutate per-step fields, and ``post_decode()``
consumes final latents. This state object is also the cache unit for
future continuous batching.
This dataclass keeps only the minimal cross-model state required by the
step-execution contract. Pipeline-specific state should be stored in
``extra`` and promoted here only when it becomes shared across models.
Examples:
- Wan-style pipelines may keep ``condition``, ``first_frame_mask``, or
``image_embeds`` in ``extra``.
- Bagel-style pipelines may keep ``gen_context``,
``cfg_text_context``, ``cfg_img_context``, or ``image_shape`` in
``extra``.
"""
# ── Identity / request-level inputs ──
request_id: str
sampling: OmniDiffusionSamplingParams
prompts: list[OmniPromptType] | None = None
# ── Encoded prompts (set once by prepare_encode) ──
prompt_embeds: torch.Tensor | None = None
prompt_embeds_mask: torch.Tensor | None = None
negative_prompt_embeds: torch.Tensor | None = None
negative_prompt_embeds_mask: torch.Tensor | None = None
# ── Latent state (mutated every step by step_scheduler) ──
latents: torch.Tensor | None = None
# ── Timestep schedule (set once by prepare_encode) ──
timesteps: torch.Tensor | list[torch.Tensor] | None = None
step_index: int = 0
# ── Per-request scheduler instance (set once by prepare_encode) ──
scheduler: Any | None = None
# ── CFG config (set once by prepare_encode) ──
do_true_cfg: bool = False
guidance: torch.Tensor | None = None
# ── Spatial / sequence metadata (set once by prepare_encode) ──
img_shapes: list | None = None
txt_seq_lens: list[int] | None = None
negative_txt_seq_lens: list[int] | None = None
# Pipeline-specific extras. Keep model-private fields here unless they
# become part of the shared step-execution contract.
# For example: Wan condition tensors / masks, or Bagel KV contexts.
extra: dict[str, Any] = field(default_factory=dict)
# ── Properties ──
@property
def current_timestep(self) -> torch.Tensor | None:
if self.timesteps is None:
return None
if self.step_index >= self.total_steps:
return None
if isinstance(self.timesteps, torch.Tensor):
if self.timesteps.ndim == 0:
return self.timesteps
return self.timesteps[self.step_index]
return self.timesteps[self.step_index]
@property
def total_steps(self) -> int:
if self.timesteps is None:
return 0
if isinstance(self.timesteps, torch.Tensor):
if self.timesteps.ndim == 0:
return 1
return int(self.timesteps.shape[0])
return len(self.timesteps)
@property
def denoise_completed(self) -> bool:
total_steps = self.total_steps
if total_steps == 0:
return False
return self.step_index >= total_steps
@property
def new_request(self) -> bool:
# TODO: this is only an approximation for current stepwise mode.
# A real "new request" signal should eventually come from scheduler/runner state transitions.
return self.step_index == 0 or self.timesteps is None
class BaseRunnerOutput(ABC):
@abstractmethod
def get_request_output(self, request_id: str) -> RunnerOutput | None:
pass
@dataclass
class RunnerOutput(BaseRunnerOutput):
"""Output of a single denoising step for a request.
NOTE: `latents` may be None when returned through IPC to avoid
serialization overhead. The actual latents are kept in Worker's
_request_state_cache.
"""
request_id: str
step_index: int | None = None
finished: bool = False
result: DiffusionOutput | None = None
def get_request_output(self, request_id: str) -> RunnerOutput | None:
return self if self.request_id == request_id else None
@dataclass
class BatchRunnerOutput(BaseRunnerOutput):
runner_outputs: list[RunnerOutput]
_id_to_idx: dict[str, int] = field(init=False, repr=False)
def __post_init__(self) -> None:
self._id_to_idx = {out.request_id: i for i, out in enumerate(self.runner_outputs)}
def __getitem__(self, request_id: str) -> RunnerOutput | None:
"""access single RunnerOutput by request_id"""
idx = self._id_to_idx.get(request_id)
return self.runner_outputs[idx] if idx is not None else None
def get_request_output(self, request_id: str) -> RunnerOutput | None:
return self[request_id]
@property
def request_ids(self) -> list[str]:
return list(self._id_to_idx.keys())
def __len__(self) -> int:
return len(self.runner_outputs)
@classmethod
def from_list(cls, runner_output_list: list[RunnerOutput]) -> BatchRunnerOutput:
return cls(runner_outputs=runner_output_list)