Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
84c6059
[kv_offload] Add tiered KV offloading framework
ronensc Apr 16, 2026
cc1ae59
Fix rebase issues: remove block_size
ronensc Apr 20, 2026
c3d6cb6
Fix rebase issues: add ReqContext and update lookup()
ronensc Apr 20, 2026
f05638b
Fix rebase issues: make pre-commit happy
ronensc Apr 20, 2026
7d3798d
Address review: pass filtered keys to submit_load
ronensc Apr 21, 2026
7374bf2
propagate request context to submit_load()
ronensc Apr 21, 2026
51654d9
Addredd review: Refactor get_primary_kv_tensor() -> create_kv_memoryv…
ronensc Apr 21, 2026
9dc1757
Address review: remove support for store_threshold
ronensc Apr 21, 2026
93a2a67
Change SecondaryTierManager.lookup() to accept single key instead of …
ronensc Apr 23, 2026
f1975be
Add ReqContext to SecondaryTierManager.lookup()
ronensc Apr 23, 2026
70bfa59
Change _initiate_promotion() to handle single key
ronensc Apr 23, 2026
f8a0438
Address review: re-organize file tree structure
ronensc Apr 23, 2026
98ffa48
Address review: remove tier_name from config
ronensc Apr 28, 2026
b8897c3
Address review: rename dummy -> example
ronensc Apr 28, 2026
56c3735
Remove unused ExampleLoadStoreSpec class
ronensc Apr 28, 2026
675e056
Address review: widen JobMetadata.keys from list to Sequence
ronensc Apr 28, 2026
a9426da
Address review: replace `LoadStoreSpec` with `block_ids: np.ndarray`
ronensc Apr 29, 2026
2ba6523
Make pre-commit happy
ronensc Apr 29, 2026
4d63460
Merge remote-tracking branch 'origin/main' into tier-offload
ronensc Apr 29, 2026
bafc17e
Fix merge issues: Iterable to Sequence
ronensc Apr 29, 2026
34605d7
Merge remote-tracking branch 'origin/main' into tier-offload
ronensc Apr 30, 2026
ad714be
Fix merge issues: update imports
ronensc Apr 30, 2026
4efc9e5
Merge branch 'main' into tier-offload
ronensc May 1, 2026
2029d46
Fix merge issues: update imports
ronensc May 3, 2026
282cfe7
Fix merge issues: change keys type
ronensc May 4, 2026
6ffec8b
Fix merge issues: sequence -> collection
ronensc May 4, 2026
e80f48b
Change SecondaryTierManager.lookup() to accept single key instead of …
ronensc May 4, 2026
6875f73
Remove unnecessary list materialism
ronensc May 4, 2026
7e77867
Refactor tiering.lookup()
ronensc May 4, 2026
82af98a
cherry-pick: Update lookup() to return None when block isn't ready
ronensc May 6, 2026
b4df136
batch and defer submit_load per tier and request
ronensc May 6, 2026
83375fb
Merge branch 'main' into tier-offload
ronensc May 7, 2026
eac6e0f
cherry-pick: Add ReqContext to touch, complete_load, complete_store
ronensc Apr 30, 2026
80b671d
Add context to tiering offloading manager methods
ronensc May 7, 2026
727d9cf
Invoke _process_finished_jobs() once per engine step
ronensc May 7, 2026
1c15206
Remove redundant tests
ronensc May 7, 2026
7f472ca
Merge branch 'main' into tier-offload
ronensc May 8, 2026
e1cf201
Address review: update docstrings of SecondaryTierManager methods
ronensc May 11, 2026
9a64da1
Address review: remove hedging language
ronensc May 11, 2026
d72847e
Address feedback: clarify a comment
ronensc May 11, 2026
489a960
Address review: save the extra function call by alias
ronensc May 11, 2026
ab56407
Address review: assert zero-copy access to primary kv tensor
ronensc May 11, 2026
be11bf9
Fix merge issue
ronensc May 11, 2026
8aac05e
Address review: unify _store_jobs and _load_jobs into _transfer_jobs
ronensc May 11, 2026
f19ba04
Address review: replace error logging with assert
ronensc May 11, 2026
3e187e1
Rename primary_store_result-> primary_write_result
ronensc May 11, 2026
cf2cc5e
Address review: return False in lookup() when primary is full
ronensc May 11, 2026
c199492
Address review: remove in-flight block tracking from example
ronensc May 11, 2026
bb56ba5
Merge remote-tracking branch 'origin/main' into tier-offload
ronensc May 12, 2026
d73e4f5
Address review: rename _PendingPromotion -> PendingPromotion
ronensc May 12, 2026
9644068
Address review: add test_tiering_offloading()
ronensc May 12, 2026
b7cc952
Address review: refactor lookup
ronensc May 12, 2026
7cc7326
Address review: wire primary_kv_memoryview during init
ronensc May 12, 2026
1129c1d
Add shutdown to SecondaryTierManager
ronensc May 13, 2026
c1e0a25
Pass vllm_config to SecondaryTierManager
ronensc May 13, 2026
d63db1e
Address review: move create_kv_memoryview to SharedOffloadRegion
ronensc May 13, 2026
414def6
cherry-pick: Add req_id to ReqContext for per-request tracking
ronensc May 13, 2026
8ef126b
Address review: use req_id instead of id(req_context) for pending pro…
ronensc May 13, 2026
6a2bfc6
Address review: replace `from __future__ import annotations` with str…
ronensc May 13, 2026
100418f
Address review: make mmap_region non-optional in CPUPrimaryTierOffloa…
ronensc May 13, 2026
648746e
Merge branch 'main' into tier-offload
ronensc May 13, 2026
f0782e6
Fix mypy errors
ronensc May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
460 changes: 460 additions & 0 deletions tests/v1/kv_offload/test_tiering_offloading.py

Large diffs are not rendered by default.

158 changes: 156 additions & 2 deletions vllm/v1/kv_offload/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

from abc import ABC, abstractmethod
from collections.abc import Iterable
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any, NewType

# `OffloadKey` identifies an offloaded block. It combines a block hash with
Expand Down Expand Up @@ -57,6 +57,10 @@ class ReqContext:
kv_transfer_params: dict[str, Any] | None = None


# Type alias for job IDs used in async transfer tracking
JobId = int


class LoadStoreSpec(ABC):
"""
Abstract metadata that encapsulates information allowing a worker
Expand Down Expand Up @@ -88,6 +92,24 @@ class OffloadingEvent:
removed: bool


@dataclass
class JobMetadata:
"""Metadata for an in-flight async transfer job."""

job_id: JobId
keys: list[OffloadKey]
Comment thread
ronensc marked this conversation as resolved.
Outdated
spec: LoadStoreSpec
Comment thread
ronensc marked this conversation as resolved.
Outdated
req_context: ReqContext = field(default_factory=ReqContext)


@dataclass
class JobResult:
"""Result of an async transfer job (successful or failed)."""

job_id: JobId
success: bool
Comment thread
ronensc marked this conversation as resolved.
Outdated


class OffloadingManager(ABC):
@abstractmethod
def lookup(self, key: OffloadKey, req_context: ReqContext) -> bool | None:
Expand Down Expand Up @@ -174,7 +196,7 @@ def complete_store(self, keys: Iterable[OffloadKey], success: bool = True):
"""
Marks blocks which were previously prepared to be stored, as stored.
Following this call, the blocks become loadable.
If if_success is False, blocks that were not marked as stored will be
If success is False, blocks that were not marked as stored will be
removed.

Args:
Expand All @@ -195,3 +217,135 @@ def take_events(self) -> Iterable[OffloadingEvent]:
def shutdown(self) -> None:
"""Shutdown the manager and release any resources."""
return


class SecondaryTierManager(ABC):
"""
Abstract interface for managing a single non-primary offloading tier.

Secondary tiers cannot directly access GPU memory. All data transfers
must go through the primary tier (implemented as CPU in current version):
Comment thread
ronensc marked this conversation as resolved.
Outdated
- Store: GPU → primary → secondary (cascade)
- Load: secondary → primary → GPU (promotion)

IMPORTANT: All methods run in the Scheduler process and must be
lightweight and non-blocking. submit_load() and submit_store() submit
async jobs; get_finished() polls for completion.
"""

@abstractmethod
def lookup(self, keys: Iterable[OffloadKey]) -> int | None:
"""
Check which blocks exist in this secondary tier.

Args:
keys: Offload keys to look up.

Returns:
Number of consecutive blocks (from start) that are present and ready,
or None if blocks are being transferred (retry later).
"""
pass

@abstractmethod
def submit_store(self, job_metadata: JobMetadata) -> None:
"""
Submit an async job to store blocks from the primary tier to this
secondary tier.

This method is lightweight: it allocates metadata and submits the
transfer job, but does NOT perform the actual data transfer on the
calling thread.

The caller (TieringOffloadingManager) must have already called
Comment thread
ronensc marked this conversation as resolved.
Outdated
primary.prepare_read(keys) to obtain job_metadata.spec and
to increment ref_cnt on those blocks. ref_cnt will be decremented
when get_finished() reports this job_id as complete and
primary.unprepare_read() is called.

This method is responsible for:
1. Filtering out blocks already present in this secondary tier
2. Evicting blocks from this secondary tier if needed (secondary
tiers are responsible for their own evictions)
3. Allocating space in this secondary tier
4. Submitting the async transfer: primary → secondary

Args:
job_metadata: Job metadata including job_id, keys, and
spec for reading blocks from the primary tier
(obtained via primary.prepare_read()).
spec is a CPULoadStoreSpec with block_ids.
"""
pass

@abstractmethod
def submit_load(self, job_metadata: JobMetadata) -> None:
"""
Submit an async job to load blocks from this secondary tier to the
primary tier.

This method is lightweight: it marks blocks as in-flight and submits
the transfer job, but does NOT perform the actual data transfer on
the calling thread.

The caller (TieringOffloadingManager) must have already called
primary.prepare_write(keys) to obtain job_metadata.spec and
to allocate space in the primary tier. When get_finished() reports
this job_id as complete, primary.complete_write() is called to make
the blocks available for GPU loads.

Args:
job_metadata: Job metadata including job_id, keys, and
spec for writing blocks into the primary tier
(obtained via primary.prepare_write()).
spec is a CPULoadStoreSpec with block_ids.
"""
pass

@abstractmethod
def get_finished(self) -> Iterable[JobResult]:
"""
Poll for finished async jobs (both loads and stores).

This is the mechanism by which the TieringOffloadingManager learns
that a transfer has finished and can:
- Call primary.unprepare_read() to decrement ref_cnt (for stores)
- Call primary.complete_write() to make blocks loadable (for loads)

Returns:
Iterable of JobResult objects for all jobs that have
finished since the last call.
"""
pass

def set_primary_view(self, view: memoryview) -> None:
"""
Provide a long-lived memoryview of the primary-tier CPU tensor.

Called once by TieringOffloadingManager during initialisation.
Override to store the view for use in `submit_store` and `submit_load`.
Use `view.strides[0]` to obtain the byte stride between block slots.

Args:
view: Memoryview of the primary tier's CPU KV cache tensor.
"""
return

def touch(self, keys: Iterable[OffloadKey]):
"""
Mark blocks as recently used for eviction policy.

Args:
keys: Offload keys to mark as recently used.
"""
return

@abstractmethod
Comment thread
ronensc marked this conversation as resolved.
Outdated
def get_tier_name(self) -> str:
"""
Get the name of this tier (e.g., "Storage", "Network").

Returns:
Tier name string.
"""
pass
51 changes: 31 additions & 20 deletions vllm/v1/kv_offload/cpu/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ def __init__(self, vllm_config: VllmConfig, kv_cache_config: KVCacheConfig):
if kv_bytes_per_offloaded_block > 0
else 0
)
world_size = vllm_config.parallel_config.world_size
self.cpu_page_size_per_worker: int = (
kv_bytes_per_offloaded_block // world_size if world_size > 0 else 0
)

# scheduler-side
self._manager: OffloadingManager | None = None
Expand All @@ -49,6 +53,22 @@ def __init__(self, vllm_config: VllmConfig, kv_cache_config: KVCacheConfig):

self.eviction_policy: str = self.extra_config.get("eviction_policy", "lru")

def _maybe_apply_store_filter(
self, manager: OffloadingManager
) -> OffloadingManager:
# store_threshold: how many times a block must appear in lookup()
# before it is eligible for CPU offloading. Values < 2 disable
# filtering (a threshold of 1 equals no filter; 0 is the default).
store_threshold = int(self.extra_config.get("store_threshold", 0))
if store_threshold >= 2:
max_tracker_size = int(self.extra_config.get("max_tracker_size", 64_000))
return FilterReusedOffloadingManager(
backing=manager,
store_threshold=store_threshold,
max_tracker_size=max_tracker_size,
)
return manager

def get_manager(self) -> OffloadingManager:
if not self._manager:
kv_events_config = self.vllm_config.kv_events_config
Expand All @@ -61,22 +81,18 @@ def get_manager(self) -> OffloadingManager:
cache_policy=self.eviction_policy, # type: ignore[arg-type]
enable_events=enable_events,
)

# store_threshold: how many times a block must appear in lookup()
# before it is eligible for CPU offloading. Values < 2 disable
# filtering (a threshold of 1 equals no filter; 0 is the default).
store_threshold = int(self.extra_config.get("store_threshold", 0))
if store_threshold >= 2:
max_tracker_size = int(
self.extra_config.get("max_tracker_size", 64_000)
)
self._manager = FilterReusedOffloadingManager(
backing=self._manager,
store_threshold=store_threshold,
max_tracker_size=max_tracker_size,
)
self._manager = self._maybe_apply_store_filter(self._manager)
return self._manager

def _create_handlers(
self, kv_caches: CanonicalKVCaches
) -> CpuGpuOffloadingHandlers:
return CpuGpuOffloadingHandlers(
kv_caches=kv_caches,
block_size_factor=self.block_size_factor,
num_cpu_blocks=self.num_blocks,
)

def get_handlers(
self, kv_caches: CanonicalKVCaches
) -> Iterator[tuple[type[LoadStoreSpec], type[LoadStoreSpec], OffloadingHandler]]:
Expand All @@ -85,12 +101,7 @@ def get_handlers(
raise Exception(
"CPU Offloading is currently only supported on CUDA-alike GPUs"
)

self._handlers = CpuGpuOffloadingHandlers(
kv_caches=kv_caches,
block_size_factor=self.block_size_factor,
num_cpu_blocks=self.num_blocks,
)
self._handlers = self._create_handlers(kv_caches)

assert self._handlers is not None
yield GPULoadStoreSpec, CPULoadStoreSpec, self._handlers.gpu_to_cpu_handler
Expand Down
5 changes: 5 additions & 0 deletions vllm/v1/kv_offload/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,8 @@ def create_spec(
OffloadingSpecFactory.register_spec(
"CPUOffloadingSpec", "vllm.v1.kv_offload.cpu.spec", "CPUOffloadingSpec"
)
OffloadingSpecFactory.register_spec(
"TieringOffloadingSpec",
"vllm.v1.kv_offload.tiering.spec",
"TieringOffloadingSpec",
)
18 changes: 18 additions & 0 deletions vllm/v1/kv_offload/secondary_tiers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Secondary tier implementations for KV cache offloading.

This package contains various secondary tier storage backends that can be used
with the TieringOffloadingManager for multi-tier KV cache management.
"""

from vllm.v1.kv_offload.secondary_tiers.dummy import (
DummyLoadStoreSpec,
DummySecondaryTier,
)

__all__ = [
"DummyLoadStoreSpec",
"DummySecondaryTier",
]
Loading
Loading