[KV Offload] Pass `ScheduleEndContext` to `on_schedule_end` hook (#46450)
Signed-off-by: Ronen Schaffer <ronen.schaffer@ibm.com> Co-authored-by: Or Ozeri <oro@il.ibm.com>
Ronen Schaffer committed
Jun 30, 2026 at 17:07 UTC
0fc251209454db524e15487bbca3b0bb5451ae8b
11 files changed
+42
-14
tests/v1/kv_offload/tiering/p2p/test_manager.py
+7
-2
@@ -13,7 +13,7 @@ from types import SimpleNamespace
13
14
import numpy as np
15
16
-from vllm.v1.kv_offload.base import LookupResult, ReqContext
16
+from vllm.v1.kv_offload.base import LookupResult, ReqContext, ScheduleEndContext
17
from vllm.v1.kv_offload.tiering.base import JobMetadata, JobResult
18
from vllm.v1.kv_offload.tiering.p2p import manager as manager_module
19
from vllm.v1.kv_offload.tiering.p2p.manager import (
@@ -1279,7 +1279,12 @@ class TestOnScheduleEnd:
1279
mgr = _make_manager()
1280
before_sessions = dict(mgr._sessions)
1281
before_jobs = list(mgr._finished_jobs)
1282
- assert mgr.on_schedule_end() is None
1282
+ assert (
1283
+ mgr.on_schedule_end(
1284
+ ScheduleEndContext(new_req_ids=[], preempted_req_ids=())
1285
+ )
1286
+ is None
1287
+ )
1288
assert mgr._sessions == before_sessions
1289
assert mgr._finished_jobs == before_jobs
1290
tests/v1/kv_offload/tiering/test_fs_tier.py
+2
-1
@@ -22,6 +22,7 @@ from vllm.v1.kv_offload.base import (
22
LookupResult,
23
OffloadKey,
24
ReqContext,
25
+ ScheduleEndContext,
26
make_offload_key,
27
)
28
from vllm.v1.kv_offload.tiering.base import JobMetadata
@@ -107,7 +108,7 @@ def lookup_and_wait(
108
"""Perform a full async lookup cycle and return resolved results."""
109
for k in keys:
110
tier.lookup(k, ctx)
110
- tier.on_schedule_end()
111
+ tier.on_schedule_end(ScheduleEndContext(new_req_ids=[], preempted_req_ids=()))
112
deadline = time.monotonic() + timeout
113
while time.monotonic() < deadline:
114
if not tier._lookup_manager._pending_results.empty():
tests/v1/kv_offload/tiering/test_obj_tier.py
+2
-1
@@ -21,6 +21,7 @@ from vllm.v1.kv_offload.base import (
21
LookupResult,
22
OffloadKey,
23
ReqContext,
24
+ ScheduleEndContext,
25
make_offload_key,
26
)
27
from vllm.v1.kv_offload.tiering.base import JobMetadata, JobResult
@@ -222,7 +223,7 @@ def lookup_and_wait(
223
"""Perform a full async lookup cycle and return resolved results."""
224
for k in keys:
225
tier.lookup(k, ctx)
225
- tier.on_schedule_end()
226
+ tier.on_schedule_end(ScheduleEndContext(new_req_ids=[], preempted_req_ids=()))
227
deadline = time.monotonic() + timeout
228
while time.monotonic() < deadline:
229
if not tier._lookup_manager._pending_results.empty():
tests/v1/kv_offload/tiering/test_tiering_offloading.py
+3
-1
@@ -27,6 +27,7 @@ from vllm.v1.kv_offload.base import (
27
OffloadPolicy,
28
ReqContext,
29
RequestOffloadingContext,
30
+ ScheduleEndContext,
31
make_offload_key,
32
)
33
from vllm.v1.kv_offload.tiering.base import (
@@ -233,7 +234,8 @@ class TestTieringOffloadingManager:
234
235
def _simulate_on_schedule_end(self):
236
"""Simulate end of scheduler step: lifecycle flush + drain events."""
236
- self.manager.on_schedule_end()
237
+ ctx = ScheduleEndContext(new_req_ids=[], preempted_req_ids=())
238
+ self.manager.on_schedule_end(ctx)
239
list(self.manager.take_events())
240
241
def _start_request(self, req_context: ReqContext = _CTX):
vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
+6
-1
@@ -42,6 +42,7 @@ from vllm.v1.kv_offload.base import (
42
OffloadPolicy,
43
ReqContext,
44
RequestOffloadingContext,
45
+ ScheduleEndContext,
46
make_offload_key,
47
)
48
from vllm.v1.outputs import KVConnectorOutput
@@ -1027,7 +1028,11 @@ class OffloadingConnectorScheduler:
1028
self, scheduler_output: SchedulerOutput
1029
) -> KVConnectorMetadata:
1030
self._update_req_states(scheduler_output)
1030
- self.manager.on_schedule_end()
1031
+ schedule_end_context = ScheduleEndContext(
1032
+ new_req_ids=[req.req_id for req in scheduler_output.scheduled_new_reqs],
1033
+ preempted_req_ids=scheduler_output.preempted_req_ids or (),
1034
+ )
1035
+ self.manager.on_schedule_end(schedule_end_context)
1036
1037
# Flush jobs for preempted requests.
1038
for req_id in scheduler_output.preempted_req_ids or ():
vllm/v1/kv_offload/base.py
+11
-2
@@ -8,7 +8,7 @@ from abc import ABC, abstractmethod
8
from collections.abc import Collection, Iterable, Sequence
9
from dataclasses import dataclass
10
from enum import Enum, auto
11
-from typing import TYPE_CHECKING, Any, NewType
11
+from typing import TYPE_CHECKING, Any, NamedTuple, NewType
12
13
import numpy as np
14
import torch
@@ -76,6 +76,15 @@ class RequestOffloadingContext:
76
policy: OffloadPolicy = OffloadPolicy.BLOCK_LEVEL
77
78
79
+class ScheduleEndContext(NamedTuple):
80
+ """Per-step scheduling info passed to on_schedule_end()."""
81
+
82
+ # Request IDs scheduled for the first time this step.
83
+ new_req_ids: Collection[str]
84
+ # Request IDs preempted this step.
85
+ preempted_req_ids: Collection[str]
86
+
87
+
88
class LoadStoreSpec(ABC):
89
"""
90
Abstract metadata that encapsulates information allowing a worker
@@ -309,7 +318,7 @@ class OffloadingManager(ABC):
318
"""
319
return ()
320
312
- def on_schedule_end(self) -> None:
321
+ def on_schedule_end(self, context: ScheduleEndContext) -> None:
322
"""Called once at the end of each scheduler step.
323
324
Managers may override this to flush deferred work accumulated
vllm/v1/kv_offload/tiering/base.py
+2
-1
@@ -17,6 +17,7 @@ from vllm.v1.kv_offload.base import (
17
OffloadKey,
18
ReqContext,
19
RequestOffloadingContext,
20
+ ScheduleEndContext,
21
)
22
23
if TYPE_CHECKING:
@@ -209,7 +210,7 @@ class SecondaryTierManager(ABC):
210
"""
211
return
212
212
- def on_schedule_end(self) -> None:
213
+ def on_schedule_end(self, context: ScheduleEndContext) -> None:
214
"""Called once at the end of each scheduler step.
215
216
Secondary tiers may override this for per-step cleanup or
vllm/v1/kv_offload/tiering/fs/manager.py
+2
-1
@@ -38,6 +38,7 @@ from vllm.v1.kv_offload.tiering.base import (
38
JobMetadata,
39
JobResult,
40
RequestOffloadingContext,
41
+ ScheduleEndContext,
42
SecondaryTierManager,
43
)
44
from vllm.v1.kv_offload.tiering.fs.io import load_block, store_block
@@ -201,7 +202,7 @@ class FileSystemTierManager(SecondaryTierManager):
202
self._lookup_manager.cleanup(req_context.req_id)
203
204
@override
204
- def on_schedule_end(self) -> None:
205
+ def on_schedule_end(self, context: ScheduleEndContext) -> None:
206
self._lookup_manager.flush()
207
208
@override
vllm/v1/kv_offload/tiering/manager.py
+3
-2
@@ -40,6 +40,7 @@ from vllm.v1.kv_offload.base import (
40
PrepareStoreOutput,
41
ReqContext,
42
RequestOffloadingContext,
43
+ ScheduleEndContext,
44
)
45
from vllm.v1.kv_offload.cpu.common import CPULoadStoreSpec
46
from vllm.v1.kv_offload.cpu.manager import CPUOffloadingManager
@@ -603,7 +604,7 @@ class TieringOffloadingManager(OffloadingManager):
604
del self._req_state[req_id]
605
606
@override
606
- def on_schedule_end(self) -> None:
607
+ def on_schedule_end(self, context: ScheduleEndContext) -> None:
608
"""End-of-schedule hook: process finished jobs, flush deferred
609
promotions, and reset the per-step gate.
610
@@ -614,7 +615,7 @@ class TieringOffloadingManager(OffloadingManager):
615
self._processed_jobs_this_step = False
616
self._flush_pending_promotions()
617
for tier in self.secondary_tiers:
617
- tier.on_schedule_end()
618
+ tier.on_schedule_end(context)
619
620
@override
621
def has_pending_work(self) -> bool:
vllm/v1/kv_offload/tiering/obj/manager.py
+2
-1
@@ -17,6 +17,7 @@ from vllm.v1.kv_offload.tiering.base import (
17
JobMetadata,
18
JobResult,
19
RequestOffloadingContext,
20
+ ScheduleEndContext,
21
SecondaryTierManager,
22
)
23
from vllm.v1.kv_offload.tiering.obj.config import ObjStoreConfig
@@ -242,7 +243,7 @@ class ObjectStoreSecondaryTierManager(SecondaryTierManager):
243
def on_request_finished(self, req_context: ReqContext) -> None:
244
self._lookup_manager.cleanup(req_context.req_id)
245
245
- def on_schedule_end(self) -> None:
246
+ def on_schedule_end(self, context: ScheduleEndContext) -> None:
247
self._lookup_manager.flush()
248
249
def on_new_request(self, req_context: ReqContext) -> RequestOffloadingContext:
vllm/v1/kv_offload/tiering/p2p/manager.py
+2
-1
@@ -26,6 +26,7 @@ from vllm.v1.kv_offload.file_mapper import FileMapper
26
from vllm.v1.kv_offload.tiering.base import (
27
JobMetadata,
28
JobResult,
29
+ ScheduleEndContext,
30
SecondaryTierManager,
31
)
32
from vllm.v1.kv_offload.tiering.p2p.control import ControlTransport, ZmqTransport
@@ -426,7 +427,7 @@ class P2PSecondaryTierManager(SecondaryTierManager):
427
time.sleep(_DRAIN_SLEEP_S)
428
429
@override
429
- def on_schedule_end(self) -> None:
430
+ def on_schedule_end(self, context: ScheduleEndContext) -> None:
431
return
432
433
# ------------------------------------------------------------------