[Bugfix][sleep mode]: guard generation on partial wake and ensure wake idempotency (#4834)

Signed-off-by: vensen <vensenmu@gmail.com>
This commit is contained in:
Vensen authored and GitHub committed 2026-07-05 14:50:08 +00:00
1 parent d7b365df64
commit ddba6de2ab
3 files changed
+92 -6

No files matched your search

+41
View File
@@ -407,6 +407,47 @@ class TestOmniSleepMode:
diffusion_engine.shutdown() diffusion_engine.shutdown()
await asyncio.sleep(1) await asyncio.sleep(1)
@pytest.mark.asyncio
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
async def test_level2_sleep_wake_raises(self, llm_engine: AsyncOmni):
"""Regression for #4473 Repro A: wake_up() after sleep(level=2) must raise
NotImplementedError instead of silently producing corrupted output."""
try:
await llm_engine.sleep(stage_ids=[0], level=2)
with pytest.raises(NotImplementedError, match="sleep\\(level=2\\)"):
await llm_engine.wake_up(stage_ids=[0])
finally:
llm_engine.shutdown()
@pytest.mark.asyncio
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
async def test_partial_wake_blocks_generate(self, llm_engine: AsyncOmni):
"""Regression for #4473 Repro B: generate() must be rejected if kv_cache
is still asleep after wake_up(tags=["weights"]), instead of crashing with
CUDA illegal memory access."""
try:
await llm_engine.sleep(stage_ids=[0], level=1)
await llm_engine.wake_up(stage_ids=[0], tags=["weights"])
with pytest.raises(RuntimeError, match="partially or fully asleep"):
async for _ in llm_engine.generate("test", sampling_params=SamplingParams(max_tokens=4)):
pass
finally:
llm_engine.shutdown()
@pytest.mark.asyncio
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
async def test_duplicate_wake_is_idempotent(self, llm_engine: AsyncOmni):
"""Regression for #4473 Repro C: duplicate wake_up(tags=None) must be a
safe no-op instead of raising a cumem CUDA invalid argument error."""
try:
await llm_engine.sleep(stage_ids=[0], level=1)
first_acks = await llm_engine.wake_up(stage_ids=[0])
assert len(first_acks) > 0, "First wake_up() should return ACKs"
second_acks = await llm_engine.wake_up(stage_ids=[0])
assert second_acks == [], f"Duplicate wake_up() should return [] but got {second_acks}"
finally:
llm_engine.shutdown()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# H100 (or MI325) e2e: BAGEL / pure diffusion, multi-TP # H100 (or MI325) e2e: BAGEL / pure diffusion, multi-TP
+8
View File
@@ -6,6 +6,7 @@ import os
import random import random
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
from enum import Enum
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import diffusers import diffusers
@@ -1488,5 +1489,12 @@ class OmniWakeTask:
tags: list[str] | None = None tags: list[str] | None = None
class CuMemTag(str, Enum):
"""Tags representing specific CuMem allocations for sleep/wake state tracking."""
WEIGHTS = "weights"
KV_CACHE = "kv_cache"
# Special message broadcast via scheduler queues to signal worker shutdown. # Special message broadcast via scheduler queues to signal worker shutdown.
SHUTDOWN_MESSAGE = {"type": "shutdown"} SHUTDOWN_MESSAGE = {"type": "shutdown"}
+43 -6
View File
@@ -26,7 +26,7 @@ from vllm.tasks import SupportedTask
from vllm.utils import random_uuid from vllm.utils import random_uuid
from vllm.v1.engine.exceptions import EngineDeadError from vllm.v1.engine.exceptions import EngineDeadError
from vllm_omni.diffusion.data import OmniACK, OmniSleepTask, OmniWakeTask from vllm_omni.diffusion.data import CuMemTag, OmniACK, OmniSleepTask, OmniWakeTask
from vllm_omni.engine.messages import ErrorMessage, OutputMessage from vllm_omni.engine.messages import ErrorMessage, OutputMessage
from vllm_omni.entrypoints.client_request_state import ClientRequestState from vllm_omni.entrypoints.client_request_state import ClientRequestState
from vllm_omni.entrypoints.omni_base import ( from vllm_omni.entrypoints.omni_base import (
@@ -141,7 +141,8 @@ class AsyncOmni(EngineClient, OmniBase):
OmniBase.__init__(self, model=model, **kwargs) OmniBase.__init__(self, model=model, **kwargs)
self._pause_cond: asyncio.Condition = asyncio.Condition() self._pause_cond: asyncio.Condition = asyncio.Condition()
self._paused: bool = False self._paused: bool = False
self._is_sleeping: bool = False self._sleeping_tags: set[str] = set()
self._level2_sleeping: bool = False
self.final_output_task: asyncio.Task | None = None self.final_output_task: asyncio.Task | None = None
self.event_resolver = AsyncEventResolver(orchestrator=self) self.event_resolver = AsyncEventResolver(orchestrator=self)
self.config_path = self.engine.config_path self.config_path = self.engine.config_path
@@ -306,6 +307,14 @@ class AsyncOmni(EngineClient, OmniBase):
logger.debug(f"[AsyncOmni] generate() called for request {external_request_id}") logger.debug(f"[AsyncOmni] generate() called for request {external_request_id}")
_sleeping_tags = getattr(self, "_sleeping_tags", None)
if _sleeping_tags:
raise RuntimeError(
f"Generation rejected: Engine is partially or fully asleep. "
f"Currently sleeping tags: {list(_sleeping_tags)}. "
f"Please perform a full wake_up before generating."
)
# Reject diffusion list-prompt early with a clear API error. # Reject diffusion list-prompt early with a clear API error.
if isinstance(prompt, list) and any( if isinstance(prompt, list) and any(
getattr(client, "stage_type", "") == "diffusion" for client in getattr(self.engine, "stage_clients", []) getattr(client, "stage_type", "") == "diffusion" for client in getattr(self.engine, "stage_clients", [])
@@ -922,11 +931,32 @@ class AsyncOmni(EngineClient, OmniBase):
if ack is not None: if ack is not None:
await self.event_resolver.resolve(ack) await self.event_resolver.resolve(ack)
final_acks.append(ack) final_acks.append(ack)
self._is_sleeping = True if not hasattr(self, "_sleeping_tags"):
self._sleeping_tags = set()
self._sleeping_tags.update([CuMemTag.WEIGHTS.value, CuMemTag.KV_CACHE.value])
if level == 2:
self._level2_sleeping = True
return final_acks return final_acks
async def wake_up(self, stage_ids: list[int] | None = None, tags: list[str] | None = None) -> list[OmniACK]: async def wake_up(self, stage_ids: list[int] | None = None, tags: list[str] | None = None) -> list[OmniACK]:
self._final_output_handler() self._final_output_handler()
if getattr(self, "_level2_sleeping", False):
raise NotImplementedError(
"wake_up() after sleep(level=2) is not yet implemented: weights were "
"discarded from GPU and reloading from disk is not yet supported. "
"Use sleep(level=1) instead, which offloads weights to CPU RAM "
"and supports fast DMA restore."
)
_current_tags = getattr(self, "_sleeping_tags", set())
if tags is None:
requested_tags = list(_current_tags)
else:
requested_tags = [t for t in tags if t in _current_tags]
if not requested_tags:
logger.info(f"[{self._name}] Requested tags {tags} are already warm. Skipping wake_up.")
return []
if stage_ids is None: if stage_ids is None:
stage_ids = list(range(len(self.engine.stage_clients))) stage_ids = list(range(len(self.engine.stage_clients)))
total_workers = 0 total_workers = 0
@@ -940,7 +970,7 @@ class AsyncOmni(EngineClient, OmniBase):
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
self.event_resolver.watch_task(task_id, expected_count=total_workers) self.event_resolver.watch_task(task_id, expected_count=total_workers)
logger.info(f"[{self._name}] Wake-up initiated (Task: {task_id}). Awaiting {total_workers} ACKs...") logger.info(f"[{self._name}] Wake-up initiated (Task: {task_id}). Awaiting {total_workers} ACKs...")
task = OmniWakeTask(tags=tags, task_id=task_id) task = OmniWakeTask(tags=requested_tags, task_id=task_id)
rpc_results = await self.collective_rpc(method="handle_wake_task", args=(task,), stage_ids=stage_ids) rpc_results = await self.collective_rpc(method="handle_wake_task", args=(task,), stage_ids=stage_ids)
final_acks = [] final_acks = []
for stage_res in rpc_results: for stage_res in rpc_results:
@@ -951,7 +981,14 @@ class AsyncOmni(EngineClient, OmniBase):
final_acks.append(ack) final_acks.append(ack)
current_omni_platform.synchronize() current_omni_platform.synchronize()
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
self._is_sleeping = False
for t in requested_tags:
if hasattr(self, "_sleeping_tags"):
self._sleeping_tags.discard(t)
# Only clear the level-2 flag once all tags are warm, in case partial
# wake support (e.g. tags=["kv_cache"] only) is added in the future.
if not getattr(self, "_sleeping_tags", None):
self._level2_sleeping = False
logger.info(f"[{self._name}] All {len(final_acks)}/{total_workers} workers reported WARM for task {task_id}.") logger.info(f"[{self._name}] All {len(final_acks)}/{total_workers} workers reported WARM for task {task_id}.")
return final_acks return final_acks
@@ -961,7 +998,7 @@ class AsyncOmni(EngineClient, OmniBase):
TODO(AsyncOmni): query the orchestrator once all stage backends expose TODO(AsyncOmni): query the orchestrator once all stage backends expose
a real sleeping-state RPC. For now we track the requested state locally. a real sleeping-state RPC. For now we track the requested state locally.
""" """
return self._is_sleeping return bool(getattr(self, "_sleeping_tags", None))
async def add_lora(self, lora_request: LoRARequest) -> bool: async def add_lora(self, lora_request: LoRARequest) -> bool:
"""Load a new LoRA adapter into all stages. """Load a new LoRA adapter into all stages.