-
-
Notifications
You must be signed in to change notification settings - Fork 19.3k
[Core] Pluggable sleep-mode backend abstraction (RFC #34303) #44074
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
simon-mo
merged 1 commit into
vllm-project:main
from
thaw-ai:rfc-34303-sleep-mode-backend
Jul 1, 2026
+309
−4
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """CPU-only unit tests for the sleep-mode backend abstraction (RFC #34303). | ||
|
|
||
| These cover the registry/factory contract and capability flags. They do not | ||
| touch CUDA - the ``cumem`` suspend/resume path is exercised end-to-end on GPU | ||
| in ``tests/basic_correctness/test_cumem.py``. | ||
| """ | ||
|
|
||
| import pytest | ||
|
|
||
| from vllm.device_allocator.sleep_mode_backend import ( | ||
| CuMemBackend, | ||
| SleepModeBackend, | ||
| SleepModeBackendFactory, | ||
| ) | ||
|
|
||
|
|
||
| def test_cumem_is_the_default_registered_backend(): | ||
| backend_cls = SleepModeBackendFactory.get_backend_class("cumem") | ||
| assert backend_cls is CuMemBackend | ||
| assert issubclass(backend_cls, SleepModeBackend) | ||
|
|
||
|
|
||
| def test_cumem_capability_flags(): | ||
| # cumem leaves NCCL untouched but does not preserve compiled artifacts, | ||
| # graphs, or durable state - these flags are what the executor and /health | ||
| # introspect to decide reinit / persistence behavior. | ||
| assert CuMemBackend.is_supported() is True | ||
| assert CuMemBackend.preserves_nccl() is True | ||
| assert CuMemBackend.preserves_compiled_artifacts() is False | ||
| assert CuMemBackend.preserves_graphs_with_nccl() is False | ||
| assert CuMemBackend.supports_durable_storage() is False | ||
|
|
||
|
|
||
| def test_new_backend_starts_in_running_state(): | ||
| # Constructing a backend must not touch the GPU; only suspend/resume do. | ||
| assert CuMemBackend().state() == "RUNNING" | ||
|
|
||
|
|
||
| def test_unknown_backend_raises(): | ||
| with pytest.raises(ValueError, match="Unsupported sleep-mode backend"): | ||
| SleepModeBackendFactory.get_backend_class("does-not-exist") | ||
|
|
||
|
|
||
| def test_duplicate_registration_raises(): | ||
| with pytest.raises(ValueError, match="already registered"): | ||
| SleepModeBackendFactory.register_backend( | ||
| "cumem", | ||
| "vllm.device_allocator.sleep_mode_backend", | ||
| "CuMemBackend", | ||
| ) | ||
|
|
||
|
|
||
| def test_third_party_backend_registration_and_resolution(): | ||
| """A plugin registers a backend by name; the factory resolves it lazily.""" | ||
| name = "_pytest_dummy_backend" | ||
| try: | ||
| SleepModeBackendFactory.register_backend( | ||
| name, | ||
| "tests.v1.worker.test_sleep_mode_backend", | ||
| "DummyBackend", | ||
| ) | ||
| resolved = SleepModeBackendFactory.get_backend_class(name) | ||
| assert resolved is DummyBackend | ||
| assert resolved.supports_durable_storage() is True | ||
| finally: | ||
| SleepModeBackendFactory._registry.pop(name, None) | ||
|
|
||
|
|
||
| def test_suspend_resume_state_transitions(): | ||
| """Lifecycle state advances RUNNING -> SUSPENDED -> RUNNING without GPU.""" | ||
| backend = DummyBackend() | ||
| assert backend.state() == "RUNNING" | ||
| backend.suspend(level=1) | ||
| assert backend.state() == "SUSPENDED" | ||
| backend.resume() | ||
| assert backend.state() == "RUNNING" | ||
|
|
||
|
|
||
| class DummyBackend(SleepModeBackend): | ||
| """A no-GPU backend used to exercise lifecycle + registration in CPU tests.""" | ||
|
|
||
| def suspend(self, level: int = 1) -> None: | ||
| self._state = "SUSPENDED" | ||
|
|
||
| def resume(self, tags: list[str] | None = None) -> None: | ||
| self._state = "RUNNING" | ||
|
|
||
| @classmethod | ||
| def supports_durable_storage(cls) -> bool: | ||
| return True |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """Pluggable sleep-mode backends (RFC #34303). | ||
|
|
||
| vLLM's sleep/wake-up today is hard-wired to ``CuMemAllocator``: the GPU worker | ||
| calls ``allocator.sleep(...)`` / ``allocator.wake_up(...)`` directly. RFC #34303 | ||
| proposes additional mechanisms for freeing and restoring GPU state - CUDA | ||
| process checkpoint, CRIU, durable snapshot/restore - that share the *dispatch* | ||
| (``/sleep`` endpoint -> engine -> executor -> worker) but differ in *mechanism* | ||
| and in which resources they preserve (NCCL communicators, compiled kernels, | ||
| CUDA graphs, survival across process restart). | ||
|
|
||
| This module introduces a thin backend abstraction so those mechanisms can be | ||
| selected by name without changing the public API. The default ``cumem`` backend | ||
| wraps today's ``CuMemAllocator`` path 1:1, so existing users see no behavior | ||
| change. The factory mirrors ``KVConnectorFactory`` and lets third-party | ||
| backends register through a ``vllm.general_plugins`` entry point at import time. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import importlib | ||
| from abc import ABC, abstractmethod | ||
| from collections.abc import Callable | ||
| from typing import TYPE_CHECKING, Literal | ||
|
|
||
| from vllm.logger import init_logger | ||
|
|
||
| if TYPE_CHECKING: | ||
| from vllm.config.model import ModelConfig | ||
|
|
||
| logger = init_logger(__name__) | ||
|
|
||
| SleepModeState = Literal["RUNNING", "SUSPENDED", "RESUMING"] | ||
|
|
||
|
|
||
| class SleepModeBackend(ABC): | ||
| """Interface for a mechanism that frees and restores GPU state. | ||
|
|
||
| A backend owns the *mechanism* of suspend/resume. The dispatch path | ||
| (``/sleep`` endpoint -> engine -> executor -> worker) is shared across all | ||
| backends and lives outside this class. | ||
|
|
||
| Capability flags are ``@classmethod`` so callers (executor, ``/health``, | ||
| AUTO selection) can introspect a backend without instantiating it, matching | ||
| the capability-flag convention used by attention backends. | ||
| """ | ||
|
|
||
| def __init__(self) -> None: | ||
| self._state: SleepModeState = "RUNNING" | ||
|
|
||
| @abstractmethod | ||
| def suspend(self, level: int = 1) -> None: | ||
| """Free GPU state. | ||
|
|
||
| ``level`` follows existing sleep-mode semantics: level 1 offloads | ||
| weights to host RAM (restorable in-process); level 2 discards weights | ||
| (reloaded from the model source on resume). | ||
| """ | ||
| raise NotImplementedError | ||
|
|
||
| @abstractmethod | ||
| def resume(self, tags: list[str] | None = None) -> None: | ||
| """Restore previously-suspended GPU state. | ||
|
|
||
| ``tags`` optionally limits which tagged allocations are restored | ||
| (e.g. ``["weights"]`` or ``["kv_cache"]``). | ||
| """ | ||
| raise NotImplementedError | ||
|
|
||
| def state(self) -> SleepModeState: | ||
| """Current lifecycle state. Lets ``/health`` distinguish a healthy-idle | ||
| (suspended) engine from a healthy-serving one (see RFC #34303).""" | ||
| return self._state | ||
|
|
||
| # -- Capability introspection (no instance required) -- | ||
|
|
||
| @classmethod | ||
| def is_supported(cls) -> bool: | ||
| """Whether this backend can run on the current platform/driver.""" | ||
| return True | ||
|
|
||
| @classmethod | ||
| def preserves_nccl(cls) -> bool: | ||
| """If False, NCCL communicators are destroyed by ``suspend`` and the | ||
| executor must re-initialize them on ``resume``.""" | ||
| return False | ||
|
|
||
| @classmethod | ||
| def preserves_compiled_artifacts(cls) -> bool: | ||
| """If True, torch.compile / JIT kernels survive suspend/resume and need | ||
| not be recompiled on resume.""" | ||
| return False | ||
|
|
||
| @classmethod | ||
| def preserves_graphs_with_nccl(cls) -> bool: | ||
| """If True, CUDA graphs containing NCCL collectives stay valid after | ||
| resume. False when NCCL is rebuilt (embedded comm handles go stale).""" | ||
| return False | ||
|
|
||
| @classmethod | ||
| def supports_durable_storage(cls) -> bool: | ||
| """If True, suspended state can be persisted beyond the process | ||
| lifetime (disk or object storage) and restored in a new process.""" | ||
| return False | ||
|
|
||
|
|
||
| class CuMemBackend(SleepModeBackend): | ||
| """Default backend. | ||
|
|
||
| Wraps the platform sleep-mode allocator exactly as the GPU worker did | ||
| before this abstraction existed, so behavior is identical to vLLM's current | ||
| sleep/wake-up. ``get_mem_allocator_instance()`` resolves to | ||
| ``CuMemAllocator`` on CUDA and ``XpuMemAllocator`` on XPU; suspend offloads | ||
| per-allocation between GPU and host, with NCCL buffers left untouched (they | ||
| are allocated outside the allocator pool). | ||
| """ | ||
|
|
||
| def suspend(self, level: int = 1) -> None: | ||
| from vllm.device_allocator import get_mem_allocator_instance | ||
|
|
||
| self._state = "SUSPENDED" | ||
| allocator = get_mem_allocator_instance() | ||
| allocator.sleep(offload_tags=("weights",) if level == 1 else tuple()) | ||
|
|
||
| def resume(self, tags: list[str] | None = None) -> None: | ||
| from vllm.device_allocator import get_mem_allocator_instance | ||
|
|
||
| self._state = "RESUMING" | ||
| allocator = get_mem_allocator_instance() | ||
| allocator.wake_up(tags) | ||
| self._state = "RUNNING" | ||
|
|
||
| @classmethod | ||
| def preserves_nccl(cls) -> bool: | ||
| # NCCL buffers live outside CuMemAllocator's pool, so an allocator-level | ||
| # sleep leaves the communicators intact (no reinit needed on resume). | ||
| return True | ||
|
|
||
|
|
||
| class SleepModeBackendFactory: | ||
| """Registry and resolver for sleep-mode backends. | ||
|
|
||
| Mirrors ``KVConnectorFactory``: lazy module/class registration and a | ||
| built-in registry populated at import time. Third-party backends register | ||
| the same way from a ``vllm.general_plugins`` entry point. | ||
| """ | ||
|
|
||
| _registry: dict[str, Callable[[], type[SleepModeBackend]]] = {} | ||
|
|
||
| @classmethod | ||
| def register_backend(cls, name: str, module_path: str, class_name: str) -> None: | ||
| """Register a backend with a lazy-loading module and class name.""" | ||
| if name in cls._registry: | ||
| raise ValueError(f"Sleep-mode backend '{name}' is already registered.") | ||
|
|
||
| def loader() -> type[SleepModeBackend]: | ||
| module = importlib.import_module(module_path) | ||
| return getattr(module, class_name) | ||
|
|
||
| cls._registry[name] = loader | ||
|
|
||
| @classmethod | ||
| def get_backend_class(cls, name: str) -> type[SleepModeBackend]: | ||
| """Resolve a registered backend class by name.""" | ||
| if name not in cls._registry: | ||
| available = ", ".join(sorted(cls._registry)) or "<none>" | ||
| raise ValueError( | ||
| f"Unsupported sleep-mode backend '{name}'. " | ||
| f"Registered backends: {available}." | ||
| ) | ||
| return cls._registry[name]() | ||
|
|
||
| @classmethod | ||
| def create_backend(cls, model_config: ModelConfig) -> SleepModeBackend: | ||
| """Instantiate the backend selected by ``model_config``.""" | ||
| name = model_config.sleep_mode_backend | ||
| backend_cls = cls.get_backend_class(name) | ||
| if not backend_cls.is_supported(): | ||
| raise ValueError( | ||
| f"Sleep-mode backend '{name}' is not supported on this platform." | ||
| ) | ||
| logger.info("Using sleep-mode backend: %s", name) | ||
| return backend_cls() | ||
|
|
||
|
|
||
| # Register built-in backends here. Registration is lazy: only the module for the | ||
| # selected backend is imported. Third-party backends (CUDA checkpoint, CRIU, | ||
| # durable snapshot) register the same way through a vllm.general_plugins entry | ||
| # point, without changes to vLLM core. | ||
| SleepModeBackendFactory.register_backend( | ||
| "cumem", | ||
| "vllm.device_allocator.sleep_mode_backend", | ||
| "CuMemBackend", | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you please move this to an appropriate directory or change existing files instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the review and the approval. moved the tests to tests/v1/worker/ and updated the plugin-registration path to match, plus annotated the _sleep_mode_backend field to clear the mypy error. rebased onto main, CI's re-running now.