Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc45f43a6a | ||
|
|
2ec1fc3c9c | ||
|
|
9505ba30ab |
No files matched your search
@@ -2,8 +2,8 @@
|
|||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
import time
|
import time
|
||||||
from collections.abc import Iterable
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import PIL.Image
|
import PIL.Image
|
||||||
@@ -371,7 +371,13 @@ class DiffusionEngine:
|
|||||||
if hasattr(self, "executor"):
|
if hasattr(self, "executor"):
|
||||||
self.executor.shutdown()
|
self.executor.shutdown()
|
||||||
|
|
||||||
def abort(self, request_id: str | Iterable[str]) -> None:
|
def abort(self) -> None:
|
||||||
# TODO implement it
|
"""
|
||||||
logger.warning("DiffusionEngine abort is not implemented yet")
|
Abort diffusion generation requests.
|
||||||
pass
|
"""
|
||||||
|
try:
|
||||||
|
for worker_pid in self.executor.get_worker_pids():
|
||||||
|
os.kill(worker_pid, signal.SIGUSR1)
|
||||||
|
logger.info("Interrupt signal sent to worker.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to interrupt current diffusion forward: {e}")
|
||||||
@@ -84,3 +84,8 @@ class DiffusionExecutor(ABC):
|
|||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
"""Shutdown the executor and release resources."""
|
"""Shutdown the executor and release resources."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_worker_pids(self) -> None:
|
||||||
|
"""Get worker pid."""
|
||||||
|
pass
|
||||||
@@ -130,6 +130,9 @@ class MultiprocDiffusionExecutor(DiffusionExecutor):
|
|||||||
def add_req(self, request: OmniDiffusionRequest) -> DiffusionOutput:
|
def add_req(self, request: OmniDiffusionRequest) -> DiffusionOutput:
|
||||||
return self.scheduler.add_req(request)
|
return self.scheduler.add_req(request)
|
||||||
|
|
||||||
|
def get_worker_pids(self):
|
||||||
|
return [process.pid for process in self._processes]
|
||||||
|
|
||||||
def collective_rpc(
|
def collective_rpc(
|
||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
|
|||||||
@@ -72,11 +72,15 @@ def get_longcat_image_edit_pre_process_func(
|
|||||||
if "additional_information" not in prompt:
|
if "additional_information" not in prompt:
|
||||||
prompt["additional_information"] = {}
|
prompt["additional_information"] = {}
|
||||||
|
|
||||||
if raw_image is None or isinstance(raw_image, list):
|
if not raw_image: # None or empty list
|
||||||
raise ValueError(
|
raise ValueError("""Received no input image. This model requires one input image to run.""")
|
||||||
"""Received no image or a list of image. Only a single image is supported by this model."""
|
elif isinstance(raw_image, list):
|
||||||
"""Please correctly set `"multi_modal_data": {"image": <an image object or file path>, …}`"""
|
if len(raw_image) > 1:
|
||||||
)
|
raise ValueError(
|
||||||
|
"""Received multiple input images. Only a single image is supported by this model."""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raw_image = raw_image[0]
|
||||||
|
|
||||||
if isinstance(raw_image, str):
|
if isinstance(raw_image, str):
|
||||||
image = PIL.Image.open(raw_image)
|
image = PIL.Image.open(raw_image)
|
||||||
|
|||||||
@@ -75,11 +75,15 @@ def get_qwen_image_edit_pre_process_func(
|
|||||||
prompt["additional_information"] = {}
|
prompt["additional_information"] = {}
|
||||||
|
|
||||||
# Only handles single image
|
# Only handles single image
|
||||||
if raw_image is None or isinstance(raw_image, list):
|
if not raw_image: # None or empty list
|
||||||
raise ValueError(
|
raise ValueError("""Received no input image. This model requires one input image to run.""")
|
||||||
"""Received no image or a list of image. Only a single image is supported by this model."""
|
elif isinstance(raw_image, list):
|
||||||
"""Please correctly set `"multi_modal_data": {"image": <an image object or file path>, …}`"""
|
if len(raw_image) > 1:
|
||||||
)
|
raise ValueError(
|
||||||
|
"""Received multiple input images. Only a single image is supported by this model."""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raw_image = raw_image[0]
|
||||||
|
|
||||||
if isinstance(raw_image, str):
|
if isinstance(raw_image, str):
|
||||||
image = PIL.Image.open(raw_image)
|
image = PIL.Image.open(raw_image)
|
||||||
|
|||||||
@@ -79,11 +79,15 @@ def get_qwen_image_layered_pre_process_func(
|
|||||||
if "additional_information" not in prompt:
|
if "additional_information" not in prompt:
|
||||||
prompt["additional_information"] = {}
|
prompt["additional_information"] = {}
|
||||||
|
|
||||||
if raw_image is None or isinstance(raw_image, list):
|
if not raw_image: # None or empty list
|
||||||
raise ValueError(
|
raise ValueError("""Received no input image. This model requires one input image to run.""")
|
||||||
"""Received no image or a list of image. Only a single image is supported by this model."""
|
elif isinstance(raw_image, list):
|
||||||
"""Please correctly set `"multi_modal_data": {"image": <an image object or file path>, …}`"""
|
if len(raw_image) > 1:
|
||||||
)
|
raise ValueError(
|
||||||
|
"""Received multiple input images. Only a single image is supported by this model."""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raw_image = raw_image[0]
|
||||||
|
|
||||||
if isinstance(raw_image, str):
|
if isinstance(raw_image, str):
|
||||||
image = PIL.Image.open(raw_image)
|
image = PIL.Image.open(raw_image)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ to DiffusionModelRunner.
|
|||||||
|
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
from contextlib import AbstractContextManager, nullcontext
|
from contextlib import AbstractContextManager, nullcontext
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
@@ -66,6 +67,21 @@ class DiffusionWorker:
|
|||||||
self._sleep_saved_buffers: dict[str, torch.Tensor] = {}
|
self._sleep_saved_buffers: dict[str, torch.Tensor] = {}
|
||||||
self.lora_manager: DiffusionLoRAManager | None = None
|
self.lora_manager: DiffusionLoRAManager | None = None
|
||||||
self.init_device()
|
self.init_device()
|
||||||
|
# Registry signal
|
||||||
|
signal.signal(signal.SIGUSR1, self._on_interrupt)
|
||||||
|
|
||||||
|
def _on_interrupt(self, signum, frame):
|
||||||
|
# Set interrupt flag on the pipeline
|
||||||
|
# The pipeline checks this flag in the diffuse loop
|
||||||
|
if (
|
||||||
|
self.model_runner is not None
|
||||||
|
and self.model_runner.pipeline is not None
|
||||||
|
and hasattr(self.model_runner.pipeline, "_interrupt")
|
||||||
|
):
|
||||||
|
self.model_runner.pipeline._interrupt = True
|
||||||
|
logger.info("Worker: Interrupt flag set for current Running Request")
|
||||||
|
else:
|
||||||
|
logger.warning("Worker: Cannot set interrupt flag - model_runner or pipeline is None")
|
||||||
|
|
||||||
def init_device(self) -> None:
|
def init_device(self) -> None:
|
||||||
"""Initialize the device and distributed environment."""
|
"""Initialize the device and distributed environment."""
|
||||||
|
|||||||
@@ -471,6 +471,29 @@ class AsyncOmni(OmniBase):
|
|||||||
else:
|
else:
|
||||||
logger.debug(f"[{self._name}] Request {req_id} fully completed")
|
logger.debug(f"[{self._name}] Request {req_id} fully completed")
|
||||||
|
|
||||||
|
def handle_oom(self, result: dict[str, Any], stage: OmniStage, stage_id: int) -> None:
|
||||||
|
if getattr(stage, "stage_type", None) == "diffusion":
|
||||||
|
error_text = str(result.get("error", ""))
|
||||||
|
error_lower = error_text.lower()
|
||||||
|
is_oom = "out of memory" in error_lower or "cuda oom" in error_lower
|
||||||
|
if is_oom:
|
||||||
|
logger.critical(
|
||||||
|
f"[{self._name}] Diffusion stage {stage_id} reported OOM; stopping stage worker.",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
stage.stop_stage_worker()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"[{self._name}] Failed to stop diffusion stage {stage_id}: {e}",
|
||||||
|
)
|
||||||
|
logger.critical(
|
||||||
|
f"[{self._name}] Exiting API process due to diffusion stage {stage_id} OOM.",
|
||||||
|
)
|
||||||
|
|
||||||
|
import os as _os
|
||||||
|
|
||||||
|
_os._exit(1)
|
||||||
|
|
||||||
def _process_single_result(
|
def _process_single_result(
|
||||||
self,
|
self,
|
||||||
result: dict[str, Any],
|
result: dict[str, Any],
|
||||||
@@ -493,6 +516,7 @@ class AsyncOmni(OmniBase):
|
|||||||
logger.error(
|
logger.error(
|
||||||
f"[{self._name}] Stage {stage_id} error on request {req_id}: {result['error']}",
|
f"[{self._name}] Stage {stage_id} error on request {req_id}: {result['error']}",
|
||||||
)
|
)
|
||||||
|
self.handle_oom(result, stage, stage_id) # only temporary workaround for oom
|
||||||
raise RuntimeError(result)
|
raise RuntimeError(result)
|
||||||
|
|
||||||
engine_outputs = _load(result, obj_key="engine_outputs", shm_key="engine_outputs_shm")
|
engine_outputs = _load(result, obj_key="engine_outputs", shm_key="engine_outputs_shm")
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ enabling concurrent request handling and streaming generation.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import AsyncGenerator, Iterable
|
from collections.abc import AsyncGenerator
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -106,8 +107,24 @@ class AsyncOmniDiffusion:
|
|||||||
self._executor = ThreadPoolExecutor(max_workers=1)
|
self._executor = ThreadPoolExecutor(max_workers=1)
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
|
# Track active requests for abort functionality
|
||||||
|
self._active_generate_task: dict[str, asyncio.Task] = {}
|
||||||
|
self._active_engine_step_task: dict[str, asyncio.Task] = {}
|
||||||
|
self._running_lock = threading.Lock()
|
||||||
|
self._running_requests: set[str] = set()
|
||||||
|
|
||||||
logger.info("AsyncOmniDiffusion initialized with model: %s", model)
|
logger.info("AsyncOmniDiffusion initialized with model: %s", model)
|
||||||
|
|
||||||
|
def _engine_step_wrapper(self, request, request_id):
|
||||||
|
with self._running_lock:
|
||||||
|
self._running_requests.add(request_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.engine.step(request)
|
||||||
|
finally:
|
||||||
|
with self._running_lock:
|
||||||
|
self._running_requests.discard(request_id)
|
||||||
|
|
||||||
async def generate(
|
async def generate(
|
||||||
self,
|
self,
|
||||||
prompt: OmniPromptType,
|
prompt: OmniPromptType,
|
||||||
@@ -137,6 +154,10 @@ class AsyncOmniDiffusion:
|
|||||||
if lora_request is not None:
|
if lora_request is not None:
|
||||||
sampling_params.lora_request = lora_request
|
sampling_params.lora_request = lora_request
|
||||||
|
|
||||||
|
# Register this task for tracking
|
||||||
|
current_task = asyncio.current_task()
|
||||||
|
self._active_generate_task[request_id] = current_task
|
||||||
|
|
||||||
request = OmniDiffusionRequest(
|
request = OmniDiffusionRequest(
|
||||||
prompts=[prompt],
|
prompts=[prompt],
|
||||||
sampling_params=sampling_params,
|
sampling_params=sampling_params,
|
||||||
@@ -147,17 +168,18 @@ class AsyncOmniDiffusion:
|
|||||||
|
|
||||||
# Run engine in thread pool
|
# Run engine in thread pool
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
# In async mode, only a single request is submitted at a time
|
||||||
try:
|
try:
|
||||||
# In async mode, only a single request is submitted at a time
|
# In async mode, only a single request is submitted at a time
|
||||||
result = await loop.run_in_executor(
|
step_task = loop.run_in_executor(self._executor, self._engine_step_wrapper, request, request_id)
|
||||||
self._executor,
|
self._active_engine_step_task[request_id] = step_task
|
||||||
self.engine.step,
|
result = await step_task
|
||||||
request,
|
|
||||||
)
|
|
||||||
result = result[0]
|
result = result[0]
|
||||||
except Exception as e:
|
finally:
|
||||||
logger.error("Generation failed for request %s: %s", request_id, e)
|
# Unregister the request
|
||||||
raise RuntimeError(f"Diffusion generation failed: {e}") from e
|
self._active_generate_task.pop(request_id, None)
|
||||||
|
self._active_engine_step_task.pop(request_id, None)
|
||||||
|
logger.debug(f"{request_id} finished")
|
||||||
|
|
||||||
# Update request_id if needed
|
# Update request_id if needed
|
||||||
if not result.request_id:
|
if not result.request_id:
|
||||||
@@ -219,9 +241,26 @@ class AsyncOmniDiffusion:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def abort(self, request_id: str | Iterable[str]) -> None:
|
async def abort(self, request_id: str):
|
||||||
"""Abort a request."""
|
logger.info(f"Aborting {request_id}.")
|
||||||
self.engine.abort(request_id)
|
with self._running_lock:
|
||||||
|
is_running = request_id in self._running_requests
|
||||||
|
if is_running:
|
||||||
|
self.engine.abort()
|
||||||
|
else:
|
||||||
|
step_task = self._active_engine_step_task.get(request_id)
|
||||||
|
if step_task and not step_task.done():
|
||||||
|
step_task.cancel()
|
||||||
|
if is_running:
|
||||||
|
logger.info(f"{request_id} engine step is running, sending interrupt")
|
||||||
|
else:
|
||||||
|
logger.info(f"{request_id} engine step not running, cancel in queue")
|
||||||
|
|
||||||
|
gen_task = self._active_generate_task.get(request_id)
|
||||||
|
if gen_task and not gen_task.done():
|
||||||
|
gen_task.cancel()
|
||||||
|
self._active_engine_step_task.pop(request_id, None)
|
||||||
|
self._active_generate_task.pop(request_id, None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
|
|||||||
@@ -991,6 +991,7 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request)
|
|||||||
HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse},
|
HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@with_cancellation
|
||||||
async def edit_images(
|
async def edit_images(
|
||||||
raw_request: Request,
|
raw_request: Request,
|
||||||
image: list[UploadFile] | None = File(None),
|
image: list[UploadFile] | None = File(None),
|
||||||
@@ -1014,7 +1015,7 @@ async def edit_images(
|
|||||||
seed: int | None = Form(None),
|
seed: int | None = Form(None),
|
||||||
# vllm-omni extension for per-request LoRA.
|
# vllm-omni extension for per-request LoRA.
|
||||||
lora: str | None = Form(None), # Json string
|
lora: str | None = Form(None), # Json string
|
||||||
) -> ImageGenerationResponse:
|
) -> ImageGenerationResponse | None:
|
||||||
"""
|
"""
|
||||||
OpenAI-compatible image edit endpoint.
|
OpenAI-compatible image edit endpoint.
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in new issue
Block a user