2025-11-28 14:22:11 +08:00
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
|
|
2026-04-01 17:25:02 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-04-15 11:52:20 +08:00
|
|
|
import inspect
|
2026-04-01 17:25:02 +08:00
|
|
|
import queue
|
2026-03-23 20:25:32 +08:00
|
|
|
import threading
|
2025-12-08 16:19:27 +08:00
|
|
|
import time
|
2026-01-22 00:32:44 +08:00
|
|
|
from collections.abc import Iterable
|
2025-12-25 22:26:30 +08:00
|
|
|
from typing import Any
|
2025-11-28 14:22:11 +08:00
|
|
|
|
2026-03-13 21:53:06 +08:00
|
|
|
import numpy as np
|
2026-01-08 23:05:44 +08:00
|
|
|
import PIL.Image
|
2026-03-18 22:59:58 +08:00
|
|
|
import torch
|
2025-11-28 14:22:11 +08:00
|
|
|
from vllm.logger import init_logger
|
2026-04-20 23:46:05 -07:00
|
|
|
from vllm.v1.engine.exceptions import EngineDeadError
|
2025-11-28 14:22:11 +08:00
|
|
|
|
2026-04-01 17:25:02 +08:00
|
|
|
from vllm_omni.diffusion.data import (
|
|
|
|
|
DiffusionOutput,
|
|
|
|
|
DiffusionRequestAbortedError,
|
|
|
|
|
OmniDiffusionConfig,
|
|
|
|
|
)
|
2026-01-22 00:32:44 +08:00
|
|
|
from vllm_omni.diffusion.executor.abstract import DiffusionExecutor
|
2026-01-08 23:05:44 +08:00
|
|
|
from vllm_omni.diffusion.registry import (
|
|
|
|
|
DiffusionModelRegistry,
|
|
|
|
|
get_diffusion_post_process_func,
|
|
|
|
|
get_diffusion_pre_process_func,
|
|
|
|
|
)
|
2025-11-28 14:22:11 +08:00
|
|
|
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
2026-04-01 17:25:02 +08:00
|
|
|
from vllm_omni.diffusion.sched import RequestScheduler, SchedulerInterface, StepScheduler
|
|
|
|
|
from vllm_omni.diffusion.sched.interface import DiffusionRequestStatus
|
|
|
|
|
from vllm_omni.diffusion.worker.utils import RunnerOutput
|
2026-01-28 16:07:42 +08:00
|
|
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt
|
2025-12-26 20:59:33 +08:00
|
|
|
from vllm_omni.outputs import OmniRequestOutput
|
2025-11-28 14:22:11 +08:00
|
|
|
|
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2026-01-08 23:05:44 +08:00
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
2026-03-13 21:53:06 +08:00
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
2026-01-29 15:18:19 +08:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-01-24 17:27:58 +08:00
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
2025-11-28 14:22:11 +08:00
|
|
|
class DiffusionEngine:
|
|
|
|
|
"""The diffusion engine for vLLM-Omni diffusion models."""
|
|
|
|
|
|
2026-03-23 20:25:32 +08:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
od_config: OmniDiffusionConfig,
|
|
|
|
|
scheduler: SchedulerInterface | None = None,
|
|
|
|
|
):
|
2025-11-28 14:22:11 +08:00
|
|
|
"""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)
|
2025-12-08 16:19:27 +08:00
|
|
|
self.pre_process_func = get_diffusion_pre_process_func(od_config)
|
2026-04-15 11:52:20 +08:00
|
|
|
# Cache whether the model-specific postprocess accepts request-level
|
|
|
|
|
# sampling params so step() can support both legacy and extended hooks.
|
|
|
|
|
self._post_process_accepts_sampling_params = bool(
|
|
|
|
|
self.post_process_func is not None
|
|
|
|
|
and "sampling_params" in inspect.signature(self.post_process_func).parameters
|
|
|
|
|
)
|
2025-11-28 14:22:11 +08:00
|
|
|
|
2026-01-22 00:32:44 +08:00
|
|
|
executor_class = DiffusionExecutor.get_class(od_config)
|
|
|
|
|
self.executor = executor_class(od_config)
|
2026-04-01 17:25:02 +08:00
|
|
|
self.step_execution = bool(getattr(od_config, "step_execution", False))
|
|
|
|
|
self.scheduler: SchedulerInterface = scheduler or (
|
|
|
|
|
StepScheduler() if self.step_execution else RequestScheduler()
|
|
|
|
|
)
|
2026-03-23 20:25:32 +08:00
|
|
|
self.scheduler.initialize(od_config)
|
2026-04-01 17:25:02 +08:00
|
|
|
self._rpc_lock = threading.RLock()
|
|
|
|
|
self.abort_queue: queue.Queue[str] = queue.Queue()
|
|
|
|
|
self.execute_fn = self.executor.execute_step if self.step_execution else self.executor.execute_request
|
2026-01-22 00:32:44 +08:00
|
|
|
|
2026-01-08 23:05:44 +08:00
|
|
|
try:
|
|
|
|
|
self._dummy_run()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Dummy run failed: {e}")
|
|
|
|
|
self.close()
|
|
|
|
|
raise e
|
2025-11-28 14:22:11 +08:00
|
|
|
|
2026-01-28 16:07:42 +08:00
|
|
|
def step(self, request: OmniDiffusionRequest) -> list[OmniRequestOutput]:
|
2026-03-09 19:03:51 +08:00
|
|
|
diffusion_engine_start_time = time.perf_counter()
|
|
|
|
|
|
2026-01-25 23:04:11 +08:00
|
|
|
# Apply pre-processing if available
|
2026-03-09 19:03:51 +08:00
|
|
|
preprocess_time = 0.0
|
2026-01-25 23:04:11 +08:00
|
|
|
if self.pre_process_func is not None:
|
2026-03-09 19:03:51 +08:00
|
|
|
preprocess_start_time = time.perf_counter()
|
2026-01-28 16:07:42 +08:00
|
|
|
request = self.pre_process_func(request)
|
2026-03-09 19:03:51 +08:00
|
|
|
preprocess_time = time.perf_counter() - preprocess_start_time
|
2026-01-25 23:04:11 +08:00
|
|
|
logger.info(f"Pre-processing completed in {preprocess_time:.4f} seconds")
|
2025-12-08 16:19:27 +08:00
|
|
|
|
2026-03-09 19:03:51 +08:00
|
|
|
exec_start_time = time.perf_counter()
|
2026-01-28 16:07:42 +08:00
|
|
|
output = self.add_req_and_wait_for_response(request)
|
2026-03-09 19:03:51 +08:00
|
|
|
exec_total_time = time.perf_counter() - exec_start_time
|
|
|
|
|
|
2026-04-01 17:25:02 +08:00
|
|
|
if output.aborted:
|
|
|
|
|
raise DiffusionRequestAbortedError(output.abort_message or "Diffusion request aborted.")
|
2026-01-25 23:04:11 +08:00
|
|
|
if output.error:
|
2026-04-20 23:46:05 -07:00
|
|
|
raise RuntimeError(output.error)
|
2026-01-25 23:04:11 +08:00
|
|
|
logger.info("Generation completed successfully.")
|
2025-12-08 16:19:27 +08:00
|
|
|
|
2026-01-25 23:04:11 +08:00
|
|
|
if output.output is None:
|
|
|
|
|
logger.warning("Output is None, returning empty OmniRequestOutput")
|
2026-01-28 16:07:42 +08:00
|
|
|
return [
|
|
|
|
|
OmniRequestOutput.from_diffusion(
|
|
|
|
|
request_id=request.request_ids[i] if i < len(request.request_ids) else "",
|
2026-01-25 23:04:11 +08:00
|
|
|
images=[],
|
|
|
|
|
prompt=prompt,
|
|
|
|
|
metrics={},
|
|
|
|
|
latents=None,
|
|
|
|
|
)
|
2026-01-28 16:07:42 +08:00
|
|
|
for i, prompt in enumerate(request.prompts)
|
|
|
|
|
]
|
2026-01-25 23:04:11 +08:00
|
|
|
|
2026-03-18 22:59:58 +08:00
|
|
|
# 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()
|
|
|
|
|
|
2026-03-09 19:03:51 +08:00
|
|
|
postprocess_start_time = time.perf_counter()
|
2026-04-15 11:52:20 +08:00
|
|
|
if self.post_process_func is not None:
|
|
|
|
|
# Some video pipelines need request-level controls during
|
|
|
|
|
# postprocess (for example worker-side frame interpolation).
|
|
|
|
|
if self._post_process_accepts_sampling_params:
|
|
|
|
|
outputs = self.post_process_func(output_data, sampling_params=request.sampling_params)
|
|
|
|
|
else:
|
|
|
|
|
outputs = self.post_process_func(output_data)
|
|
|
|
|
else:
|
|
|
|
|
outputs = output_data
|
2026-03-05 23:31:48 +08:00
|
|
|
audio_payload = None
|
2026-04-15 11:52:20 +08:00
|
|
|
custom_output = output.custom_output or {}
|
2026-04-09 14:52:12 +08:00
|
|
|
model_audio_sample_rate = None
|
|
|
|
|
model_fps = None
|
2026-03-05 23:31:48 +08:00
|
|
|
if isinstance(outputs, dict):
|
|
|
|
|
audio_payload = outputs.get("audio")
|
2026-04-15 11:52:20 +08:00
|
|
|
custom_output.update(outputs.get("custom_output") or {})
|
2026-04-09 14:52:12 +08:00
|
|
|
model_audio_sample_rate = outputs.get("audio_sample_rate")
|
|
|
|
|
model_fps = outputs.get("fps")
|
2026-03-05 23:31:48 +08:00
|
|
|
outputs = outputs.get("video", outputs)
|
2026-03-09 19:03:51 +08:00
|
|
|
postprocess_time = time.perf_counter() - postprocess_start_time
|
2026-01-25 23:04:11 +08:00
|
|
|
logger.info(f"Post-processing completed in {postprocess_time:.4f} seconds")
|
|
|
|
|
|
2026-03-09 19:03:51 +08:00
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-25 23:04:11 +08:00
|
|
|
# Convert to OmniRequestOutput format
|
|
|
|
|
# Ensure outputs is a list
|
|
|
|
|
if not isinstance(outputs, list):
|
|
|
|
|
outputs = [outputs] if outputs is not None else []
|
|
|
|
|
|
2026-02-09 19:17:32 +08:00
|
|
|
metrics = {
|
2026-03-09 19:03:51 +08:00
|
|
|
"preprocess_time_ms": preprocess_time * 1000,
|
2026-04-27 21:40:31 +08:00
|
|
|
"diffusion_engine_exec_time_ms": exec_total_time * 1000,
|
|
|
|
|
"diffusion_engine_total_time_ms": step_total_ms,
|
2026-02-09 19:17:32 +08:00
|
|
|
"image_num": int(request.sampling_params.num_outputs_per_prompt),
|
|
|
|
|
"resolution": int(request.sampling_params.resolution),
|
2026-03-07 19:11:47 +08:00
|
|
|
"postprocess_time_ms": postprocess_time * 1000,
|
2026-02-09 19:17:32 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-25 23:04:11 +08:00
|
|
|
# Handle single request or multiple requests
|
2026-04-27 21:40:31 +08:00
|
|
|
is_audio_output = supports_audio_output(self.od_config.model_class_name)
|
2026-01-28 16:07:42 +08:00
|
|
|
if len(request.prompts) == 1:
|
2026-01-25 23:04:11 +08:00
|
|
|
# Single request: return single OmniRequestOutput
|
2026-01-28 16:07:42 +08:00
|
|
|
prompt = request.prompts[0]
|
|
|
|
|
request_id = request.request_ids[0] if request.request_ids else ""
|
2026-01-25 23:04:11 +08:00
|
|
|
|
2026-04-27 21:40:31 +08:00
|
|
|
if is_audio_output:
|
2026-03-05 23:31:48 +08:00
|
|
|
request_audio_payload = outputs[0] if len(outputs) == 1 else outputs
|
2026-01-28 16:07:42 +08:00
|
|
|
return [
|
|
|
|
|
OmniRequestOutput.from_diffusion(
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
images=[],
|
|
|
|
|
prompt=prompt,
|
|
|
|
|
metrics=metrics,
|
|
|
|
|
latents=output.trajectory_latents,
|
2026-04-09 18:06:40 +08:00
|
|
|
trajectory_latents=output.trajectory_latents,
|
|
|
|
|
trajectory_timesteps=output.trajectory_timesteps,
|
|
|
|
|
trajectory_log_probs=output.trajectory_log_probs,
|
|
|
|
|
trajectory_decoded=output.trajectory_decoded,
|
2026-03-05 23:31:48 +08:00
|
|
|
multimodal_output={"audio": request_audio_payload},
|
2026-01-28 16:07:42 +08:00
|
|
|
final_output_type="audio",
|
2026-03-19 12:26:12 +08:00
|
|
|
stage_durations=output.stage_durations,
|
2026-03-24 00:19:20 +08:00
|
|
|
peak_memory_mb=output.peak_memory_mb,
|
2026-01-28 16:07:42 +08:00
|
|
|
),
|
|
|
|
|
]
|
2026-01-25 23:04:11 +08:00
|
|
|
else:
|
2026-03-05 23:31:48 +08:00
|
|
|
mm_output = {}
|
|
|
|
|
if audio_payload is not None:
|
|
|
|
|
mm_output["audio"] = audio_payload
|
2026-04-09 14:52:12 +08:00
|
|
|
if model_audio_sample_rate is not None:
|
|
|
|
|
mm_output["audio_sample_rate"] = model_audio_sample_rate
|
|
|
|
|
if model_fps is not None:
|
|
|
|
|
mm_output["fps"] = model_fps
|
2026-01-28 16:07:42 +08:00
|
|
|
return [
|
|
|
|
|
OmniRequestOutput.from_diffusion(
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
images=outputs,
|
|
|
|
|
prompt=prompt,
|
|
|
|
|
metrics=metrics,
|
|
|
|
|
latents=output.trajectory_latents,
|
2026-04-08 10:43:01 +08:00
|
|
|
trajectory_latents=output.trajectory_latents,
|
|
|
|
|
trajectory_timesteps=output.trajectory_timesteps,
|
|
|
|
|
trajectory_log_probs=output.trajectory_log_probs,
|
|
|
|
|
trajectory_decoded=output.trajectory_decoded,
|
2026-04-15 11:52:20 +08:00
|
|
|
custom_output=custom_output,
|
2026-03-05 23:31:48 +08:00
|
|
|
multimodal_output=mm_output,
|
2026-03-19 12:26:12 +08:00
|
|
|
stage_durations=output.stage_durations,
|
2026-03-24 00:19:20 +08:00
|
|
|
peak_memory_mb=output.peak_memory_mb,
|
2026-01-28 16:07:42 +08:00
|
|
|
),
|
|
|
|
|
]
|
2026-01-25 23:04:11 +08:00
|
|
|
else:
|
|
|
|
|
# Multiple requests: return list of OmniRequestOutput
|
|
|
|
|
# Split images based on num_outputs_per_prompt for each request
|
|
|
|
|
results = []
|
|
|
|
|
output_idx = 0
|
|
|
|
|
|
2026-01-28 16:07:42 +08:00
|
|
|
for i, prompt in enumerate(request.prompts):
|
|
|
|
|
request_id = request.request_ids[i] if i < len(request.request_ids) else ""
|
2026-01-25 23:04:11 +08:00
|
|
|
|
|
|
|
|
# Get images for this request
|
2026-01-28 16:07:42 +08:00
|
|
|
num_outputs = request.sampling_params.num_outputs_per_prompt
|
2026-03-05 23:31:48 +08:00
|
|
|
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
|
2025-12-26 20:59:33 +08:00
|
|
|
|
2026-04-27 21:40:31 +08:00
|
|
|
if is_audio_output:
|
2026-03-05 23:31:48 +08:00
|
|
|
request_audio_payload = request_outputs[0] if len(request_outputs) == 1 else request_outputs
|
2026-01-25 23:04:11 +08:00
|
|
|
results.append(
|
|
|
|
|
OmniRequestOutput.from_diffusion(
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
images=[],
|
|
|
|
|
prompt=prompt,
|
|
|
|
|
metrics=metrics,
|
|
|
|
|
latents=output.trajectory_latents,
|
2026-04-09 18:06:40 +08:00
|
|
|
trajectory_latents=output.trajectory_latents,
|
|
|
|
|
trajectory_timesteps=output.trajectory_timesteps,
|
|
|
|
|
trajectory_log_probs=output.trajectory_log_probs,
|
|
|
|
|
trajectory_decoded=output.trajectory_decoded,
|
2026-03-05 23:31:48 +08:00
|
|
|
multimodal_output={"audio": request_audio_payload},
|
2026-01-25 23:04:11 +08:00
|
|
|
final_output_type="audio",
|
2026-03-19 12:26:12 +08:00
|
|
|
stage_durations=output.stage_durations,
|
2026-03-24 00:19:20 +08:00
|
|
|
peak_memory_mb=output.peak_memory_mb,
|
2026-03-05 23:31:48 +08:00
|
|
|
),
|
2026-01-24 17:27:58 +08:00
|
|
|
)
|
|
|
|
|
else:
|
2026-03-05 23:31:48 +08:00
|
|
|
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
|
2026-04-09 14:52:12 +08:00
|
|
|
if model_audio_sample_rate is not None:
|
|
|
|
|
mm_output["audio_sample_rate"] = model_audio_sample_rate
|
|
|
|
|
if model_fps is not None:
|
|
|
|
|
mm_output["fps"] = model_fps
|
2026-01-25 23:04:11 +08:00
|
|
|
results.append(
|
|
|
|
|
OmniRequestOutput.from_diffusion(
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
images=request_outputs,
|
|
|
|
|
prompt=prompt,
|
|
|
|
|
metrics=metrics,
|
|
|
|
|
latents=output.trajectory_latents,
|
2026-04-08 10:43:01 +08:00
|
|
|
trajectory_latents=output.trajectory_latents,
|
|
|
|
|
trajectory_timesteps=output.trajectory_timesteps,
|
|
|
|
|
trajectory_log_probs=output.trajectory_log_probs,
|
|
|
|
|
trajectory_decoded=output.trajectory_decoded,
|
2026-04-15 11:52:20 +08:00
|
|
|
custom_output=custom_output,
|
2026-03-05 23:31:48 +08:00
|
|
|
multimodal_output=mm_output,
|
2026-03-19 12:26:12 +08:00
|
|
|
stage_durations=output.stage_durations,
|
2026-03-24 00:19:20 +08:00
|
|
|
peak_memory_mb=output.peak_memory_mb,
|
2026-03-05 23:31:48 +08:00
|
|
|
),
|
2026-01-25 23:04:11 +08:00
|
|
|
)
|
2025-12-26 20:59:33 +08:00
|
|
|
|
2026-03-07 19:11:47 +08:00
|
|
|
return results
|
2025-11-28 14:22:11 +08:00
|
|
|
|
|
|
|
|
@staticmethod
|
2026-03-23 20:25:32 +08:00
|
|
|
def make_engine(
|
|
|
|
|
config: OmniDiffusionConfig,
|
|
|
|
|
scheduler: SchedulerInterface | None = None,
|
2026-04-01 17:25:02 +08:00
|
|
|
) -> DiffusionEngine:
|
2025-11-28 14:22:11 +08:00
|
|
|
"""Factory method to create a DiffusionEngine instance.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
config: The configuration for the diffusion engine.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
An instance of DiffusionEngine.
|
|
|
|
|
"""
|
2026-03-23 20:25:32 +08:00
|
|
|
return DiffusionEngine(config, scheduler=scheduler)
|
2025-11-28 14:22:11 +08:00
|
|
|
|
2026-03-23 20:25:32 +08:00
|
|
|
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:
|
2026-04-01 17:25:02 +08:00
|
|
|
self._process_aborts_queue()
|
2026-03-23 20:25:32 +08:00
|
|
|
sched_output = self.scheduler.schedule()
|
|
|
|
|
if sched_output.is_empty:
|
2026-04-01 17:25:02 +08:00
|
|
|
if target_sched_req_id in sched_output.finished_req_ids:
|
|
|
|
|
return self._finalize_finished_request(target_sched_req_id)
|
2026-03-23 20:25:32 +08:00
|
|
|
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]
|
|
|
|
|
try:
|
2026-04-01 17:25:02 +08:00
|
|
|
runner_output = self.execute_fn(sched_output)
|
2026-04-20 23:46:05 -07:00
|
|
|
except EngineDeadError:
|
|
|
|
|
raise
|
2026-03-23 20:25:32 +08:00
|
|
|
except Exception as exc:
|
2026-04-01 17:25:02 +08:00
|
|
|
logger.error("Execution failed for diffusion request %s", sched_req_id, exc_info=True)
|
|
|
|
|
runner_output = RunnerOutput(
|
|
|
|
|
req_id=sched_req_id,
|
|
|
|
|
step_index=None,
|
|
|
|
|
finished=True,
|
|
|
|
|
result=DiffusionOutput(error=str(exc)),
|
2026-03-23 20:25:32 +08:00
|
|
|
)
|
|
|
|
|
|
2026-04-01 17:25:02 +08:00
|
|
|
self._process_aborts_queue()
|
|
|
|
|
|
|
|
|
|
finished_req_ids = self.scheduler.update_from_output(sched_output, runner_output)
|
2026-03-23 20:25:32 +08:00
|
|
|
if target_sched_req_id in finished_req_ids:
|
2026-04-01 17:25:02 +08:00
|
|
|
return self._finalize_finished_request(
|
|
|
|
|
target_sched_req_id,
|
|
|
|
|
runner_output=runner_output,
|
|
|
|
|
missing_result_error="Diffusion execution finished without a final output.",
|
|
|
|
|
)
|
2025-12-08 11:52:07 +08:00
|
|
|
|
2026-03-24 15:25:52 +08:00
|
|
|
def profile(self, is_start: bool = True, profile_prefix: str | None = None) -> None:
|
2026-04-10 18:13:12 +08:00
|
|
|
"""Start or stop profiling on all diffusion workers.
|
2026-01-20 11:35:48 +08:00
|
|
|
|
|
|
|
|
Args:
|
2026-03-24 15:25:52 +08:00
|
|
|
is_start: True to start profiling, False to stop.
|
2026-04-10 18:13:12 +08:00
|
|
|
profile_prefix: Optional prefix for trace filename.
|
2026-01-20 11:35:48 +08:00
|
|
|
"""
|
2026-03-24 15:25:52 +08:00
|
|
|
if is_start:
|
|
|
|
|
if profile_prefix is None:
|
|
|
|
|
profile_prefix = f"diffusion_{int(time.time())}"
|
|
|
|
|
logger.info(f"Starting diffusion profiling with prefix: {profile_prefix}")
|
2026-01-20 11:35:48 +08:00
|
|
|
else:
|
2026-03-24 15:25:52 +08:00
|
|
|
logger.info("Stopping diffusion profiling...")
|
2026-01-20 11:35:48 +08:00
|
|
|
|
2026-03-24 15:25:52 +08:00
|
|
|
try:
|
|
|
|
|
self.collective_rpc(method="profile", args=(is_start, profile_prefix))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
action = "start" if is_start else "stop"
|
|
|
|
|
logger.error(f"Failed to {action} profiling on workers", exc_info=True)
|
|
|
|
|
if is_start:
|
|
|
|
|
raise RuntimeError(f"Could not {action} profiler: {e}") from e
|
2026-01-20 11:35:48 +08:00
|
|
|
|
2026-01-08 23:05:44 +08:00
|
|
|
def _dummy_run(self):
|
|
|
|
|
"""A dummy run to warm up the model."""
|
|
|
|
|
num_inference_steps = 1
|
2026-04-04 15:03:32 +08:00
|
|
|
height = 512
|
|
|
|
|
width = 512
|
2026-01-08 23:05:44 +08:00
|
|
|
if supports_image_input(self.od_config.model_class_name):
|
|
|
|
|
# Provide a dummy image input if the model supports it
|
2026-01-29 15:18:19 +08:00
|
|
|
color_format = image_color_format(self.od_config.model_class_name)
|
|
|
|
|
dummy_image = PIL.Image.new(color_format, (width, height))
|
2026-01-08 23:05:44 +08:00
|
|
|
else:
|
|
|
|
|
dummy_image = None
|
2026-03-13 21:53:06 +08:00
|
|
|
|
|
|
|
|
if supports_audio_input(self.od_config.model_class_name):
|
|
|
|
|
audio_sr = 16000
|
2026-04-27 21:40:31 +08:00
|
|
|
dummy_audio = np.random.randn(audio_sr * 2).astype(np.float32)
|
2026-03-13 21:53:06 +08:00
|
|
|
else:
|
|
|
|
|
dummy_audio = None
|
|
|
|
|
|
|
|
|
|
prompt: OmniTextPrompt = {
|
|
|
|
|
"prompt": "dummy run",
|
|
|
|
|
"multi_modal_data": {"image": dummy_image, "audio": dummy_audio},
|
|
|
|
|
}
|
2026-01-08 23:05:44 +08:00
|
|
|
req = OmniDiffusionRequest(
|
2026-01-28 16:07:42 +08:00
|
|
|
prompts=[prompt],
|
2026-03-23 20:25:32 +08:00
|
|
|
request_ids=["dummy_req_id"],
|
2026-01-28 16:07:42 +08:00
|
|
|
sampling_params=OmniDiffusionSamplingParams(
|
|
|
|
|
height=height,
|
|
|
|
|
width=width,
|
|
|
|
|
num_inference_steps=num_inference_steps,
|
2026-02-13 18:05:07 +08:00
|
|
|
# Keep warmup path minimal and robust across text encoders.
|
|
|
|
|
# Some models may fail when warmup implicitly triggers
|
|
|
|
|
# classifier-free guidance with an empty negative prompt.
|
|
|
|
|
guidance_scale=0.0,
|
2026-01-28 16:07:42 +08:00
|
|
|
num_outputs_per_prompt=1,
|
2026-03-06 09:55:54 +08:00
|
|
|
# Disable CFG for warmup to avoid triggering CFG parallel
|
|
|
|
|
# validation when cfg_parallel_size > 1.
|
|
|
|
|
extra_args={"cfg_text_scale": 1.0, "cfg_img_scale": 1.0},
|
2026-01-28 16:07:42 +08:00
|
|
|
),
|
2026-01-08 23:05:44 +08:00
|
|
|
)
|
|
|
|
|
logger.info("dummy run to warm up the model")
|
2026-01-28 16:07:42 +08:00
|
|
|
request = self.pre_process_func(req) if self.pre_process_func is not None else req
|
2026-03-23 20:25:32 +08:00
|
|
|
output = self.add_req_and_wait_for_response(request)
|
|
|
|
|
if output.error:
|
|
|
|
|
raise RuntimeError(f"Dummy run failed: {output.error}")
|
2026-01-08 23:05:44 +08:00
|
|
|
|
2025-12-25 22:26:30 +08:00
|
|
|
def collective_rpc(
|
|
|
|
|
self,
|
2026-01-22 00:32:44 +08:00
|
|
|
method: str,
|
2025-12-25 22:26:30 +08:00
|
|
|
timeout: float | None = None,
|
|
|
|
|
args: tuple = (),
|
|
|
|
|
kwargs: dict | None = None,
|
|
|
|
|
unique_reply_rank: int | None = None,
|
|
|
|
|
) -> Any:
|
|
|
|
|
"""Call a method on worker processes and get results immediately.
|
|
|
|
|
|
|
|
|
|
Args:
|
2026-01-22 00:32:44 +08:00
|
|
|
method: The method name (str) to execute on workers
|
2025-12-25 22:26:30 +08:00
|
|
|
timeout: Optional timeout in seconds
|
|
|
|
|
args: Positional arguments for the method
|
|
|
|
|
kwargs: Keyword arguments for the method
|
|
|
|
|
unique_reply_rank: If set, only get reply from this rank
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Single result if unique_reply_rank is provided, otherwise list of results
|
|
|
|
|
"""
|
2026-01-22 00:32:44 +08:00
|
|
|
assert isinstance(method, str), "Only string method names are supported for now"
|
2026-03-23 20:25:32 +08:00
|
|
|
|
|
|
|
|
deadline = None if timeout is None else time.monotonic() + timeout
|
|
|
|
|
acquired = False
|
|
|
|
|
try:
|
|
|
|
|
if deadline is None:
|
|
|
|
|
self._rpc_lock.acquire()
|
|
|
|
|
acquired = True
|
|
|
|
|
else:
|
|
|
|
|
lock_timeout = max(0, deadline - time.monotonic())
|
|
|
|
|
acquired = self._rpc_lock.acquire(timeout=lock_timeout)
|
|
|
|
|
if not acquired:
|
|
|
|
|
raise TimeoutError(f"RPC call to {method} timed out waiting for engine lock.")
|
|
|
|
|
|
|
|
|
|
rpc_timeout = None if deadline is None else max(0, deadline - time.monotonic())
|
|
|
|
|
if deadline is not None and rpc_timeout <= 0:
|
|
|
|
|
raise TimeoutError(f"RPC call to {method} timed out.")
|
|
|
|
|
|
|
|
|
|
return self.executor.collective_rpc(
|
|
|
|
|
method=method,
|
|
|
|
|
timeout=rpc_timeout,
|
|
|
|
|
args=args,
|
|
|
|
|
kwargs=kwargs,
|
|
|
|
|
unique_reply_rank=unique_reply_rank,
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
if acquired:
|
|
|
|
|
self._rpc_lock.release()
|
2025-12-25 22:26:30 +08:00
|
|
|
|
2025-12-25 11:32:42 +08:00
|
|
|
def close(self) -> None:
|
2026-03-23 20:25:32 +08:00
|
|
|
if hasattr(self, "scheduler"):
|
|
|
|
|
self.scheduler.close()
|
2026-01-22 00:32:44 +08:00
|
|
|
if hasattr(self, "executor"):
|
|
|
|
|
self.executor.shutdown()
|
2026-01-05 14:53:10 +08:00
|
|
|
|
|
|
|
|
def abort(self, request_id: str | Iterable[str]) -> None:
|
2026-04-01 17:25:02 +08:00
|
|
|
request_ids = [request_id] if isinstance(request_id, str) else list(request_id)
|
|
|
|
|
for req_id in request_ids:
|
|
|
|
|
self.abort_queue.put(req_id)
|
|
|
|
|
|
|
|
|
|
def _process_aborts_queue(self) -> None:
|
|
|
|
|
if self.abort_queue.empty():
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
request_ids: list[str] = []
|
|
|
|
|
while not self.abort_queue.empty():
|
|
|
|
|
ids = self.abort_queue.get_nowait()
|
|
|
|
|
request_ids.extend((ids,) if isinstance(ids, str) else ids)
|
|
|
|
|
|
|
|
|
|
self._abort_requests(request_ids)
|
|
|
|
|
|
|
|
|
|
def _abort_requests(self, request_ids: str | Iterable[str]) -> None:
|
|
|
|
|
request_ids = [request_ids] if isinstance(request_ids, str) else list(request_ids)
|
|
|
|
|
|
|
|
|
|
sched_req_ids: list[str] = []
|
|
|
|
|
for request_id in dict.fromkeys(request_ids):
|
|
|
|
|
sched_req_id = self.scheduler.get_sched_req_id(request_id)
|
|
|
|
|
if sched_req_id is not None:
|
|
|
|
|
sched_req_ids.append(sched_req_id)
|
|
|
|
|
|
|
|
|
|
for sched_req_id in dict.fromkeys(sched_req_ids):
|
|
|
|
|
if self.scheduler.get_request_state(sched_req_id) is not None:
|
|
|
|
|
self.scheduler.finish_requests(sched_req_id, DiffusionRequestStatus.FINISHED_ABORTED)
|
|
|
|
|
|
|
|
|
|
def _finalize_finished_request(
|
|
|
|
|
self,
|
|
|
|
|
sched_req_id: str,
|
|
|
|
|
runner_output: RunnerOutput | None = None,
|
|
|
|
|
missing_result_error: str = "Diffusion scheduler finished target request without execution output.",
|
|
|
|
|
) -> DiffusionOutput:
|
|
|
|
|
state = self.scheduler.get_request_state(sched_req_id)
|
|
|
|
|
popped_state = self.scheduler.pop_request_state(sched_req_id)
|
|
|
|
|
state = state or popped_state
|
|
|
|
|
|
|
|
|
|
if state is None:
|
|
|
|
|
raise RuntimeError(f"Diffusion scheduler lost state for request {sched_req_id}.")
|
|
|
|
|
|
|
|
|
|
if state.status == DiffusionRequestStatus.FINISHED_ABORTED:
|
|
|
|
|
request_id = state.req.request_ids[0] if state.req.request_ids else sched_req_id
|
|
|
|
|
return DiffusionOutput(
|
|
|
|
|
aborted=True,
|
|
|
|
|
abort_message=f"Request {request_id} aborted.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if runner_output is not None and runner_output.result is not None:
|
|
|
|
|
return runner_output.result
|
|
|
|
|
|
|
|
|
|
return DiffusionOutput(error=missing_result_error)
|