[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()
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
+8
View File
@@ -6,6 +6,7 @@ import os
import random
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field, fields
from enum import Enum
from typing import TYPE_CHECKING, Any
import diffusers
@@ -1488,5 +1489,12 @@ class OmniWakeTask:
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.
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.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.entrypoints.client_request_state import ClientRequestState
from vllm_omni.entrypoints.omni_base import (
@@ -141,7 +141,8 @@ class AsyncOmni(EngineClient, OmniBase):
OmniBase.__init__(self, model=model, **kwargs)
self._pause_cond: asyncio.Condition = asyncio.Condition()
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.event_resolver = AsyncEventResolver(orchestrator=self)
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}")
_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.
if isinstance(prompt, list) and any(
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:
await self.event_resolver.resolve(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
async def wake_up(self, stage_ids: list[int] | None = None, tags: list[str] | None = None) -> list[OmniACK]:
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:
stage_ids = list(range(len(self.engine.stage_clients)))
total_workers = 0
@@ -940,7 +970,7 @@ class AsyncOmni(EngineClient, OmniBase):
task_id = str(uuid.uuid4())
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...")
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)
final_acks = []
for stage_res in rpc_results:
@@ -951,7 +981,14 @@ class AsyncOmni(EngineClient, OmniBase):
final_acks.append(ack)
current_omni_platform.synchronize()
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}.")
return final_acks
@@ -961,7 +998,7 @@ class AsyncOmni(EngineClient, OmniBase):
TODO(AsyncOmni): query the orchestrator once all stage backends expose
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:
"""Load a new LoRA adapter into all stages.