Compare commits

...
Author SHA1 Message Date
dengyunyang dc45f43a6a [Feature] Abort diffusion request (#1156)
Signed-off-by: dengyunyang <584797741@qq.com>
2026-02-03 15:49:58 +08:00
Jiangyun Zhu 2ec1fc3c9c [Bugfix] exit when oom (#1163)
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
2026-02-03 15:03:54 +08:00
dengyunyangandHuang, Zeyu 9505ba30ab [Bugfix][cherry-pick]: /images/edits endpoint fails pipeline data format check (#1173)
Signed-off-by: Huang, Zeyu <11222265+fhfuih@users.noreply.github.com>
Co-authored-by: Huang, Zeyu <11222265+fhfuih@users.noreply.github.com>
2026-02-03 13:55:19 +08:00
10 changed files with 139 additions and 33 deletions

No files matched your search

+11 -5
View File
@@ -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}")
+5
View File
@@ -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."""
+24
View File
@@ -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")
+51 -12
View File
@@ -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:
+2 -1
View File
@@ -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.
""" """