Files
vllm-omni/vllm_omni/diffusion/prompt_update.py
T

246 lines
10 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Midway prompt updates for chunked diffusion step execution.
``PromptUpdateMixin`` exposes
- a pipeline interface for the external to queue a prompt update,
- a pipeline internal method to apply prompt transitions at chunk boundaries.
``prompt_update_versions``: a helper for ``InputBatch`` cache invalidation.
"""
from __future__ import annotations
import logging
from collections.abc import Sequence
from dataclasses import dataclass
from threading import Lock
from typing import ClassVar, TypedDict, cast
import torch
from vllm_omni.diffusion.worker.utils import StepRequestState
DEFAULT_TRANSITION_CHUNKS = 3
logger = logging.getLogger(__name__)
class PromptUpdateExtra(TypedDict, total=False):
"""A "protocol" for ``StepRequestState.extra`` keys used by the prompt-update feature."""
pending_prompt_update: _PendingPromptUpdate
prompt_update_state: _PromptUpdateState
prompt_update_chunk_metadata: _PromptUpdateChunkMetadata
prompt_update_version: int # Bumped when prompt_embeds change; invalidates InputBatch cache.
# The lock for storing/updating pending prompt updates.
# Saved in state.extra to be naturally cleaned up with the request state.
# As PromptUpdateMixin (the pipeline) currently lacks an obvious request-finish hook
prompt_update_lock: Lock
def prompt_update_versions(states: Sequence[StepRequestState]) -> tuple[int, ...]:
"""Return per-request prompt-update versions for batch cache comparison.
To be used by ``InputBatch`` or other external to determine if the cached batch is still valid.
"""
return tuple(int(cast(PromptUpdateExtra, state.extra).get("prompt_update_version", 0)) for state in states)
class PromptUpdateMixin:
"""Mixin for chunked streaming pipelines that support midway prompt updates.
Implements the behavior expected by :class:`~vllm_omni.diffusion.models.interface.SupportsPromptUpdate`.
All per-request transition state lives on ``StepRequestState.extra``; this mixin itself is stateless.
"""
supports_prompt_update: ClassVar[bool] = True
@staticmethod
def _prompt_update_lock(extra: PromptUpdateExtra) -> Lock:
return extra.setdefault("prompt_update_lock", Lock())
def prepare_prompt_update(
self,
state: StepRequestState,
prompt: str,
event_id: str,
transition_chunks: int | None = None,
) -> None:
"""Encode and queue a prompt update to apply at the next chunk boundary.
It does not actually apply the prompt transition.
This is an extended interface for the pipeline, to be called by the runner.
"""
if not prompt:
raise ValueError("prompt must be non-empty")
if not event_id:
raise ValueError("event_id must be non-empty")
if state.prompt_embeds is None:
raise ValueError(
f"prompt_update is not allowed before initial generation has started (request_id={state.request_id!r})"
)
duration = DEFAULT_TRANSITION_CHUNKS if transition_chunks is None else transition_chunks
if duration < 0:
raise ValueError("transition_chunks must be >= 0")
target_prompt_embeds, _ = self.encode_prompt(
prompt=prompt,
negative_prompt=None,
do_classifier_free_guidance=False,
num_videos_per_prompt=state.sampling.num_outputs_per_prompt,
max_sequence_length=state.sampling.max_sequence_length,
device=self.device,
dtype=self.transformer.dtype,
)
extra = cast(PromptUpdateExtra, state.extra)
with self._prompt_update_lock(extra):
extra["pending_prompt_update"] = {
"event_id": event_id,
"prompt": prompt,
"target_prompt_embeds": target_prompt_embeds,
"transition_chunks": duration,
}
def _apply_prompt_update_at_chunk_boundary(self, state: StepRequestState) -> None:
"""Advance or start prompt interpolation before the next chunk."""
extra = cast(PromptUpdateExtra, state.extra)
update_state: _PromptUpdateState | None = extra.get("prompt_update_state")
with self._prompt_update_lock(extra):
pending = extra.pop("pending_prompt_update", None)
embeds_changed = False
next_chunk_index = state.chunk_index
started_event_ids: list[str] = []
active_event_ids: list[str] = []
completed_event_ids: list[str] = []
# If current transition is not complete, advance it.
# After completion, leave prompt_update_state in place (so a later pending
# update can abort/overwrite), but do not keep bumping the version.
if update_state is not None:
in_transition = (
update_state.transition_chunks > 0
and update_state.elapsed_transition_chunks < update_state.transition_chunks
)
if in_transition:
update_state.advance_transition()
state.prompt_embeds = update_state.blended_prompt_embeds()
embeds_changed = True
active_event_ids.append(update_state.event_id)
if update_state.elapsed_transition_chunks >= update_state.transition_chunks:
state.prompt_embeds = update_state.target_prompt_embeds
update_state.source_prompt_embeds = update_state.target_prompt_embeds
completed_event_ids.append(update_state.event_id)
if update_state.target_prompt is not None:
logger.debug(
"prompt_update transition complete request_id=%s next_chunk_index=%d prompt=%.20s...",
state.request_id,
next_chunk_index,
update_state.target_prompt,
)
# If a new prompt update is pending, start a new transition.
if pending is not None:
if state.prompt_embeds is None:
raise RuntimeError(
"internal error: trying to apply a pending prompt update but "
f"current prompt_embeds is None (request_id={state.request_id!r})"
)
source = state.prompt_embeds.detach().clone()
target = pending["target_prompt_embeds"]
duration = int(pending["transition_chunks"])
prompt = str(pending.get("prompt", ""))
event_id = pending.get("event_id")
update_state = _PromptUpdateState(
source_prompt_embeds=source,
target_prompt_embeds=target,
transition_chunks=duration,
event_id=event_id,
target_prompt=prompt,
)
extra["prompt_update_state"] = update_state
started_event_ids.append(event_id)
active_event_ids.append(event_id)
if duration <= 0:
# A hard/immediate transition
state.prompt_embeds = target
completed_event_ids.append(event_id)
logger.debug(
"prompt_update sharp transition request_id=%s next_chunk_index=%d prompt=%.20s...",
state.request_id,
next_chunk_index,
prompt,
)
else:
# A transition that really takes some time
update_state.advance_transition()
state.prompt_embeds = update_state.blended_prompt_embeds()
if update_state.elapsed_transition_chunks >= update_state.transition_chunks:
state.prompt_embeds = update_state.target_prompt_embeds
update_state.source_prompt_embeds = update_state.target_prompt_embeds
completed_event_ids.append(event_id)
logger.debug(
"prompt_update transition start request_id=%s next_chunk_index=%d prompt=%.20s...",
state.request_id,
next_chunk_index,
prompt,
)
embeds_changed = True
# Indicate that prompt embeddings have changed---clear input batch cache.
if embeds_changed:
new_version = int(extra.get("prompt_update_version", 0)) + 1
extra["prompt_update_version"] = new_version
extra["prompt_update_chunk_metadata"] = {
"started_event_ids": started_event_ids,
"active_event_ids": active_event_ids,
"completed_event_ids": completed_event_ids,
}
@dataclass
class _PromptUpdateState:
"""Per-request prompt interpolation stored in ``state.extra["prompt_update_state"]``."""
source_prompt_embeds: torch.Tensor
target_prompt_embeds: torch.Tensor
transition_chunks: int
event_id: str
elapsed_transition_chunks: int = 0
target_prompt: str | None = None
def blended_prompt_embeds(self) -> torch.Tensor:
alpha = self._current_alpha()
if alpha >= 1.0:
return self.target_prompt_embeds
return (1.0 - alpha) * self.source_prompt_embeds + alpha * self.target_prompt_embeds
def advance_transition(self) -> None:
if self.transition_chunks <= 0:
self.source_prompt_embeds = self.target_prompt_embeds
return
self.elapsed_transition_chunks += 1
if self.elapsed_transition_chunks >= self.transition_chunks:
self.source_prompt_embeds = self.target_prompt_embeds
self.elapsed_transition_chunks = self.transition_chunks
def _current_alpha(self) -> float:
if self.transition_chunks <= 0:
return 1.0
return min(1.0, self.elapsed_transition_chunks / self.transition_chunks)
class _PendingPromptUpdate(TypedDict):
event_id: str
prompt: str
target_prompt_embeds: torch.Tensor
transition_chunks: int
class _PromptUpdateChunkMetadata(TypedDict):
started_event_ids: list[str]
active_event_ids: list[str]
completed_event_ids: list[str]